├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── docs ├── STYLESHEETS.md └── images │ ├── error.png │ ├── help.png │ ├── placeholders.png │ ├── result.png │ ├── stylesheets │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ └── 7.png │ └── validation.png ├── index.js ├── lib ├── components.js ├── i18n │ └── en.js ├── index.js ├── stylesheets │ └── bootstrap.js ├── templates │ └── bootstrap │ │ ├── checkbox.js │ │ ├── datepicker.android.js │ │ ├── datepicker.ios.js │ │ ├── index.js │ │ ├── list.js │ │ ├── select.android.js │ │ ├── select.ios.js │ │ ├── struct.js │ │ └── textbox.js └── util.js ├── package-lock.json ├── package.json ├── test └── index.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | AwesomeProject.xcodeproj 4 | AwesomeProjectTests 5 | index.ios.js 6 | iOS 7 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | node_modules 4 | test 5 | docs 6 | AwesomeProject.xcodeproj 7 | AwesomeProjectTests 8 | index.ios.js 9 | iOS 10 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | Under Android, use the `fields` option to configure which `mode` to display the Picker: 546 | 547 | ```js 548 | // see the "Rendering options" section in this guide 549 | var options = { 550 | fields: { 551 | birthDate: { 552 | mode: 'date' // display the Date field as a DatePickerAndroid 553 | } 554 | } 555 | }; 556 | ``` 557 | 558 | #### iOS date `config` option 559 | 560 | 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: 561 | 562 | | Key | Value | 563 | |-----|-------| 564 | | `animation` | The animation to collapse the date picker. Defaults to `Animated.timing`. | 565 | | `animationConfig` | The animation configuration object. Defaults to `{duration: 200}` for the default animation. | 566 | | `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)`. 567 | | `defaultValueText` | An `string` to customize the default value of the `null` date value text. | 568 | 569 | For the collapsible customization, look at the `dateTouchable` and `dateValue` keys in the stylesheet file. 570 | 571 | #### Android date `config` option 572 | 573 | When using a `t.Date` type in Android, it can be configured through a `config` option that take the following parameters: 574 | 575 | | Key | Value | 576 | |-----|-------| 577 | | ``background`` | Determines the type of background drawable that's going to be used to display feedback. Optional, defaults to ``TouchableNativeFeedback.SelectableBackground``. | 578 | | ``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)``. 579 | | ``dialogMode`` | Determines the type of datepicker mode for Android (`default`, `spinner` or `calendar`). | 580 | | `defaultValueText` | An `string` to customize the default value of the `null` date value text. | 581 | 582 | ### Enums 583 | 584 | In order to create an enum field, use the `t.enums` combinator: 585 | 586 | ```js 587 | var Gender = t.enums({ 588 | M: 'Male', 589 | F: 'Female' 590 | }); 591 | 592 | var Person = t.struct({ 593 | name: t.String, 594 | surname: t.String, 595 | email: t.maybe(t.String), 596 | age: t.Number, 597 | rememberMe: t.Boolean, 598 | gender: Gender // enum 599 | }); 600 | ``` 601 | 602 | Enums are displayed as `Picker`s. 603 | 604 | #### iOS select `config` option 605 | 606 | 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: 607 | 608 | | Key | Value | 609 | |-----|-------| 610 | | `animation` | The animation to collapse the date picker. Defaults to `Animated.timing`. | 611 | | `animationConfig` | The animation configuration object. Defaults to `{duration: 200}` for the default animation. | 612 | 613 | For the collapsible customization, look at the `pickerContainer`, `pickerTouchable` and `pickerValue` keys in the stylesheet file. 614 | 615 | ### Refinements 616 | 617 | A *predicate* is a function with the following signature: 618 | 619 | ``` 620 | (x: any) => boolean 621 | ``` 622 | 623 | You can refine a type with the `t.refinement(type, predicate)` combinator: 624 | 625 | ```js 626 | // a type representing positive numbers 627 | var Positive = t.refinement(t.Number, function (n) { 628 | return n >= 0; 629 | }); 630 | 631 | var Person = t.struct({ 632 | name: t.String, 633 | surname: t.String, 634 | email: t.maybe(t.String), 635 | age: Positive, // refinement 636 | rememberMe: t.Boolean, 637 | gender: Gender 638 | }); 639 | ``` 640 | 641 | Subtypes allow you to express any custom validation with a simple predicate. 642 | 643 | # Rendering options 644 | 645 | In order to customize the look and feel, use an `options` prop: 646 | 647 | ```js 648 | 649 | ``` 650 | 651 | ## Form component 652 | 653 | ### Labels and placeholders 654 | 655 | By default labels are automatically generated. You can turn off this behaviour or override the default labels 656 | on field basis. 657 | 658 | ```js 659 | var options = { 660 | label: 'My struct label' // <= form legend, displayed before the fields 661 | }; 662 | 663 | var options = { 664 | fields: { 665 | name: { 666 | label: 'My name label' // <= label for the name field 667 | } 668 | } 669 | }; 670 | ``` 671 | 672 | In order to automatically generate default placeholders, use the option `auto: 'placeholders'`: 673 | 674 | ```js 675 | var options = { 676 | auto: 'placeholders' 677 | }; 678 | 679 | 680 | ``` 681 | 682 | ![Placeholders](docs/images/placeholders.png) 683 | 684 | Set `auto: 'none'` if you don't want neither labels nor placeholders. 685 | 686 | ```js 687 | var options = { 688 | auto: 'none' 689 | }; 690 | ``` 691 | 692 | ### Fields order 693 | 694 | You can sort the fields with the `order` option: 695 | 696 | ```js 697 | var options = { 698 | order: ['name', 'surname', 'rememberMe', 'gender', 'age', 'email'] 699 | }; 700 | ``` 701 | 702 | ### Default values 703 | 704 | You can set the default values passing a `value` prop to the `Form` component: 705 | 706 | ```js 707 | var value = { 708 | name: 'Giulio', 709 | surname: 'Canti', 710 | age: 41, 711 | gender: 'M' 712 | }; 713 | 714 | 715 | ``` 716 | 717 | ### Fields configuration 718 | 719 | You can configure each field with the `fields` option: 720 | 721 | ```js 722 | var options = { 723 | fields: { 724 | name: { 725 | // name field configuration here.. 726 | }, 727 | surname: { 728 | // surname field configuration here.. 729 | } 730 | } 731 | }; 732 | ``` 733 | 734 | ## Textbox component 735 | 736 | Implementation: `TextInput` 737 | 738 | **Tech note.** Values containing only white spaces are converted to `null`. 739 | 740 | ### Placeholder 741 | 742 | You can set the placeholder with the `placeholder` option: 743 | 744 | ```js 745 | var options = { 746 | fields: { 747 | name: { 748 | placeholder: 'Your placeholder here' 749 | } 750 | } 751 | }; 752 | ``` 753 | 754 | ### Label 755 | 756 | You can set the label with the `label` option: 757 | 758 | ```js 759 | var options = { 760 | fields: { 761 | name: { 762 | label: 'Insert your name' 763 | } 764 | } 765 | }; 766 | ``` 767 | 768 | ### Help message 769 | 770 | You can set a help message with the `help` option: 771 | 772 | ```js 773 | var options = { 774 | fields: { 775 | name: { 776 | help: 'Your help message here' 777 | } 778 | } 779 | }; 780 | ``` 781 | 782 | ![Help](docs/images/help.png) 783 | 784 | ### Error messages 785 | 786 | You can add a custom error message with the `error` option: 787 | 788 | ```js 789 | var options = { 790 | fields: { 791 | email: { 792 | // you can use strings or JSX 793 | error: 'Insert a valid email' 794 | } 795 | } 796 | }; 797 | ``` 798 | 799 | ![Help](docs/images/error.png) 800 | 801 | tcomb-form-native will display the error message when the field validation fails. 802 | 803 | `error` can also be a function with the following signature: 804 | 805 | ``` 806 | (value, path, context) => ?(string | ReactElement) 807 | ``` 808 | 809 | where 810 | 811 | - `value` is an object containing the current form value. 812 | - `path` is the path of the value being validated 813 | - `context` is the value of the `context` prop. Also it contains a reference to the component options. 814 | 815 | The value returned by the function will be used as error message. 816 | 817 | If you want to show the error message onload, add the `hasError` option: 818 | 819 | ```js 820 | var options = { 821 | hasError: true, 822 | error: A custom error message 823 | }; 824 | ``` 825 | 826 | Another way is to add a: 827 | 828 | ``` 829 | getValidationErrorMessage(value, path, context) 830 | ``` 831 | 832 | static function to a type, where: 833 | 834 | - `value` is the (parsed) current value of the component. 835 | - `path` is the path of the value being validated 836 | - `context` is the value of the `context` prop. Also it contains a reference to the component options. 837 | 838 | 839 | ```js 840 | var Age = t.refinement(t.Number, function (n) { return n >= 18; }); 841 | 842 | // if you define a getValidationErrorMessage function, it will be called on validation errors 843 | Age.getValidationErrorMessage = function (value, path, context) { 844 | return 'bad age, locale: ' + context.locale; 845 | }; 846 | 847 | var Schema = t.struct({ 848 | age: Age 849 | }); 850 | 851 | ... 852 | 853 | 858 | ``` 859 | 860 | You can even define `getValidationErrorMessage` on the supertype in order to be DRY: 861 | 862 | ```js 863 | t.Number.getValidationErrorMessage = function (value, path, context) { 864 | return 'bad number'; 865 | }; 866 | 867 | Age.getValidationErrorMessage = function (value, path, context) { 868 | return 'bad age, locale: ' + context.locale; 869 | }; 870 | ``` 871 | 872 | ### Other standard options 873 | 874 | The following standard options are available (see http://facebook.github.io/react-native/docs/textinput.html): 875 | 876 | - `allowFontScaling` 877 | - `autoCapitalize` 878 | - `autoCorrect` 879 | - `autoFocus` 880 | - `bufferDelay` 881 | - `clearButtonMode` 882 | - `editable` 883 | - `enablesReturnKeyAutomatically` 884 | - `keyboardType` 885 | - `maxLength` 886 | - `multiline` 887 | - `numberOfLines` 888 | - `onBlur` 889 | - `onEndEditing` 890 | - `onFocus` 891 | - `onSubmitEditing` 892 | - `onContentSizeChange` 893 | - `password` 894 | - `placeholderTextColor` 895 | - `returnKeyType` 896 | - `selectTextOnFocus` 897 | - `secureTextEntry` 898 | - `selectionState` 899 | - `textAlign` 900 | - `textAlignVertical` 901 | - `textContentType` 902 | - ~~`underlineColorAndroid`~~ 903 | 904 | `underlineColorAndroid` is not supported now on `tcomb-form-native` due to random crashes on Android, especially on ScrollView. See more on: 905 | https://github.com/facebook/react-native/issues/17530#issuecomment-416367184 906 | 907 | ## Checkbox component 908 | 909 | Implementation: `SwitchIOS` 910 | 911 | The following options are similar to the `Textbox` component's ones: 912 | 913 | - `label` 914 | - `help` 915 | - `error` 916 | 917 | ### Other standard options 918 | 919 | The following standard options are available (see http://facebook.github.io/react-native/docs/switchios.html): 920 | 921 | - `disabled` 922 | - `onTintColor` 923 | - `thumbTintColor` 924 | - `tintColor` 925 | 926 | ## Select component 927 | 928 | Implementation: `PickerIOS` 929 | 930 | The following options are similar to the `Textbox` component's ones: 931 | 932 | - `label` 933 | - `help` 934 | - `error` 935 | 936 | ### `nullOption` option 937 | 938 | You can customize the null option with the `nullOption` option: 939 | 940 | ```js 941 | var options = { 942 | fields: { 943 | gender: { 944 | nullOption: {value: '', text: 'Choose your gender'} 945 | } 946 | } 947 | }; 948 | ``` 949 | 950 | You can remove the null option setting the `nullOption` option to `false`. 951 | 952 | **Warning**: when you set `nullOption = false` you must also set the Form's `value` prop for the select field. 953 | 954 | **Tech note.** A value equal to `nullOption.value` (default `''`) is converted to `null`. 955 | 956 | ### Options order 957 | 958 | You can sort the options with the `order` option: 959 | 960 | ```js 961 | var options = { 962 | fields: { 963 | gender: { 964 | order: 'asc' // or 'desc' 965 | } 966 | } 967 | }; 968 | ``` 969 | 970 | ### Options isCollapsed 971 | 972 | You can determinate if Select is collapsed: 973 | 974 | ```js 975 | var options = { 976 | fields: { 977 | gender: { 978 | isCollapsed: false // default: true 979 | } 980 | } 981 | }; 982 | ``` 983 | 984 | If option not set, default is `true` 985 | 986 | ### Options onCollapseChange 987 | 988 | You can set a callback, triggered, when collapse change: 989 | 990 | ```js 991 | var options = { 992 | fields: { 993 | gender: { 994 | onCollapseChange: () => { console.log('collapse changed'); } 995 | } 996 | } 997 | }; 998 | ``` 999 | 1000 | ## DatePicker component 1001 | 1002 | Implementation: `DatePickerIOS` 1003 | 1004 | ### Example 1005 | 1006 | ```js 1007 | var Person = t.struct({ 1008 | name: t.String, 1009 | birthDate: t.Date 1010 | }); 1011 | ``` 1012 | 1013 | The following options are similar to the `Textbox` component's ones: 1014 | 1015 | - `label` 1016 | - `help` 1017 | - `error` 1018 | 1019 | ### Other standard options 1020 | 1021 | The following standard options are available (see http://facebook.github.io/react-native/docs/datepickerios.html): 1022 | 1023 | - `maximumDate`, 1024 | - `minimumDate`, 1025 | - `minuteInterval`, 1026 | - `mode`, 1027 | - `timeZoneOffsetInMinutes` 1028 | 1029 | ## Hidden Component 1030 | 1031 | For any component, you can set the field with the `hidden` option: 1032 | 1033 | ```js 1034 | var options = { 1035 | fields: { 1036 | name: { 1037 | hidden: true 1038 | } 1039 | } 1040 | }; 1041 | ``` 1042 | 1043 | This will completely skip the rendering of the component, while the default value will be available for validation purposes. 1044 | 1045 | # Unions 1046 | 1047 | **Code Example** 1048 | 1049 | ```js 1050 | const AccountType = t.enums.of([ 1051 | 'type 1', 1052 | 'type 2', 1053 | 'other' 1054 | ], 'AccountType') 1055 | 1056 | const KnownAccount = t.struct({ 1057 | type: AccountType 1058 | }, 'KnownAccount') 1059 | 1060 | // UnknownAccount extends KnownAccount so it owns also the type field 1061 | const UnknownAccount = KnownAccount.extend({ 1062 | label: t.String, 1063 | }, 'UnknownAccount') 1064 | 1065 | // the union 1066 | const Account = t.union([KnownAccount, UnknownAccount], 'Account') 1067 | 1068 | // the final form type 1069 | const Type = t.list(Account) 1070 | 1071 | const options = { 1072 | item: [ // one options object for each concrete type of the union 1073 | { 1074 | label: 'KnownAccount' 1075 | }, 1076 | { 1077 | label: 'UnknownAccount' 1078 | } 1079 | ] 1080 | } 1081 | ``` 1082 | 1083 | 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: 1084 | 1085 | ```js 1086 | // if account type is 'other' return the UnknownAccount type 1087 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount 1088 | ``` 1089 | 1090 | # Lists 1091 | 1092 | You can handle a list with the `t.list` combinator: 1093 | 1094 | ```js 1095 | const Person = t.struct({ 1096 | name: t.String, 1097 | tags: t.list(t.String) // a list of strings 1098 | }); 1099 | ``` 1100 | 1101 | ## Items configuration 1102 | 1103 | To configure all the items in a list, set the `item` option: 1104 | 1105 | ```js 1106 | const Person = t.struct({ 1107 | name: t.String, 1108 | tags: t.list(t.String) // a list of strings 1109 | }); 1110 | 1111 | const options = { 1112 | fields: { // <= Person options 1113 | tags: { 1114 | item: { // <= options applied to each item in the list 1115 | label: 'My tag' 1116 | } 1117 | } 1118 | } 1119 | }); 1120 | ``` 1121 | 1122 | ## Nested structures 1123 | 1124 | You can nest lists and structs at an arbitrary level: 1125 | 1126 | ```js 1127 | const Person = t.struct({ 1128 | name: t.String, 1129 | surname: t.String 1130 | }); 1131 | 1132 | const Persons = t.list(Person); 1133 | ``` 1134 | 1135 | If you want to provide options for your nested structures they must be nested 1136 | following the type structure. Here is an example: 1137 | 1138 | ```js 1139 | const Person = t.struct({ 1140 | name: t.Struct, 1141 | position: t.Struct({ 1142 | latitude: t.Number, 1143 | longitude: t.Number 1144 | }); 1145 | }); 1146 | 1147 | const options = { 1148 | fields: { // <= Person options 1149 | name: { 1150 | label: 'name label' 1151 | } 1152 | position: { 1153 | fields: { 1154 | // Note that latitude is not directly nested in position, 1155 | // but in the fields property 1156 | latitude: { 1157 | label: 'My position label' 1158 | } 1159 | } 1160 | } 1161 | } 1162 | }); 1163 | ``` 1164 | 1165 | When dealing with `t.list`, make sure to declare the `fields` property inside the `item` property, as such: 1166 | 1167 | ```js 1168 | const Documents = t.struct({ 1169 | type: t.Number, 1170 | value: t.String 1171 | }) 1172 | 1173 | const Person = t.struct({ 1174 | name: t.Struct, 1175 | documents: t.list(Documents) 1176 | }); 1177 | 1178 | const options = { 1179 | fields: { 1180 | name: { /*...*/ }, 1181 | documents: { 1182 | item: { 1183 | fields: { 1184 | type: { 1185 | // Documents t.struct 'type' options 1186 | }, 1187 | value: { 1188 | // Documents t.struct 'value' options 1189 | } 1190 | } 1191 | } 1192 | } 1193 | } 1194 | } 1195 | ``` 1196 | 1197 | ## Internationalization 1198 | 1199 | You can override the default language (english) with the `i18n` option: 1200 | 1201 | ```js 1202 | const options = { 1203 | i18n: { 1204 | optional: ' (optional)', 1205 | required: '', 1206 | add: 'Add', // add button 1207 | remove: '✘', // remove button 1208 | up: '↑', // move up button 1209 | down: '↓' // move down button 1210 | } 1211 | }; 1212 | ``` 1213 | 1214 | ## Buttons configuration 1215 | 1216 | You can prevent operations on lists with the following options: 1217 | 1218 | - `disableAdd`: (default `false`) prevents adding new items 1219 | - `disableRemove`: (default `false`) prevents removing existing items 1220 | - `disableOrder`: (default `false`) prevents sorting existing items 1221 | 1222 | ```js 1223 | const options = { 1224 | disableOrder: true 1225 | }; 1226 | ``` 1227 | 1228 | ## List with Dynamic Items (Different structs based on selected value) 1229 | 1230 | 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: 1231 | 1232 | ```js 1233 | const AccountType = t.enums.of([ 1234 | 'type 1', 1235 | 'type 2', 1236 | 'other' 1237 | ], 'AccountType') 1238 | 1239 | const KnownAccount = t.struct({ 1240 | type: AccountType 1241 | }, 'KnownAccount') 1242 | 1243 | // UnknownAccount extends KnownAccount so it owns also the type field 1244 | const UnknownAccount = KnownAccount.extend({ 1245 | label: t.String, 1246 | }, 'UnknownAccount') 1247 | 1248 | // the union 1249 | const Account = t.union([KnownAccount, UnknownAccount], 'Account') 1250 | 1251 | // the final form type 1252 | const Type = t.list(Account) 1253 | ``` 1254 | 1255 | 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: 1256 | 1257 | ```js 1258 | // if account type is 'other' return the UnknownAccount type 1259 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount 1260 | ``` 1261 | 1262 | # Customizations 1263 | 1264 | ## Stylesheets 1265 | 1266 | See also [Stylesheet guide](docs/STYLESHEETS.md). 1267 | 1268 | tcomb-form-native comes with a default style. You can customize the look and feel by setting another stylesheet: 1269 | 1270 | ```js 1271 | var t = require('tcomb-form-native/lib'); 1272 | var i18n = require('tcomb-form-native/lib/i18n/en'); 1273 | var templates = require('tcomb-form-native/lib/templates/bootstrap'); 1274 | 1275 | // define a stylesheet (see lib/stylesheets/bootstrap for an example) 1276 | var stylesheet = {...}; 1277 | 1278 | // override globally the default stylesheet 1279 | t.form.Form.stylesheet = stylesheet; 1280 | // set defaults 1281 | t.form.Form.templates = templates; 1282 | t.form.Form.i18n = i18n; 1283 | ``` 1284 | 1285 | You can also override the stylesheet locally for selected fields: 1286 | 1287 | ```js 1288 | var Person = t.struct({ 1289 | name: t.String 1290 | }); 1291 | 1292 | var options = { 1293 | fields: { 1294 | name: { 1295 | stylesheet: myCustomStylesheet 1296 | } 1297 | } 1298 | }; 1299 | ``` 1300 | 1301 | Or per form: 1302 | 1303 | ```js 1304 | var Person = t.struct({ 1305 | name: t.String 1306 | }); 1307 | 1308 | var options = { 1309 | stylesheet: myCustomStylesheet 1310 | }; 1311 | ``` 1312 | 1313 | For a complete example see the default stylesheet https://github.com/gcanti/tcomb-form-native/blob/master/lib/stylesheets/bootstrap.js. 1314 | 1315 | ## Templates 1316 | 1317 | tcomb-form-native comes with a default layout, i.e. a bunch of templates, one for each component. 1318 | When changing the stylesheet is not enough, you can customize the layout by setting custom templates: 1319 | 1320 | ```js 1321 | var t = require('tcomb-form-native/lib'); 1322 | var i18n = require('tcomb-form-native/lib/i18n/en'); 1323 | var stylesheet = require('tcomb-form-native/lib/stylesheets/bootstrap'); 1324 | 1325 | // define the templates (see lib/templates/bootstrap for an example) 1326 | var templates = {...}; 1327 | 1328 | // override globally the default layout 1329 | t.form.Form.templates = templates; 1330 | // set defaults 1331 | t.form.Form.stylesheet = stylesheet; 1332 | t.form.Form.i18n = i18n; 1333 | ``` 1334 | 1335 | You can also override the template locally: 1336 | 1337 | ```js 1338 | var Person = t.struct({ 1339 | name: t.String 1340 | }); 1341 | 1342 | function myCustomTemplate(locals) { 1343 | 1344 | var containerStyle = {...}; 1345 | var labelStyle = {...}; 1346 | var textboxStyle = {...}; 1347 | 1348 | return ( 1349 | 1350 | {locals.label} 1351 | 1352 | 1353 | ); 1354 | } 1355 | 1356 | var options = { 1357 | fields: { 1358 | name: { 1359 | template: myCustomTemplate 1360 | } 1361 | } 1362 | }; 1363 | ``` 1364 | 1365 | A template is a function with the following signature: 1366 | 1367 | ``` 1368 | (locals: Object) => ReactElement 1369 | ``` 1370 | 1371 | where `locals` is an object contaning the "recipe" for rendering the input and it's built for you by tcomb-form-native. 1372 | Let's see an example: the `locals` object passed in the `checkbox` template: 1373 | 1374 | ```js 1375 | type Message = string | ReactElement 1376 | 1377 | { 1378 | stylesheet: Object, // the styles to be applied 1379 | hasError: boolean, // true if there is a validation error 1380 | error: ?Message, // the optional error message to be displayed 1381 | label: Message, // the label to be displayed 1382 | help: ?Message, // the optional help message to be displayed 1383 | value: boolean, // the current value of the checkbox 1384 | onChange: Function, // the event handler to be called when the value changes 1385 | config: Object, // an optional object to pass configuration options to the new template 1386 | 1387 | ...other input options here... 1388 | 1389 | } 1390 | ``` 1391 | 1392 | For a complete example see the default template https://github.com/gcanti/tcomb-form-native/blob/master/lib/templates/bootstrap. 1393 | 1394 | ## i18n 1395 | 1396 | tcomb-form-native comes with a default internationalization (English). You can change it by setting another i18n object: 1397 | 1398 | ```js 1399 | var t = require('tcomb-form-native/lib'); 1400 | var templates = require('tcomb-form-native/lib/templates/bootstrap'); 1401 | 1402 | // define an object containing your translations (see tcomb-form-native/lib/i18n/en for an example) 1403 | var i18n = {...}; 1404 | 1405 | // override globally the default i18n 1406 | t.form.Form.i18n = i18n; 1407 | // set defaults 1408 | t.form.Form.templates = templates; 1409 | t.form.Form.stylesheet = stylesheet; 1410 | ``` 1411 | 1412 | ## Transformers 1413 | 1414 | Say you want a search textbox which accepts a list of keywords separated by spaces: 1415 | 1416 | ```js 1417 | var Search = t.struct({ 1418 | search: t.list(t.String) 1419 | }); 1420 | ``` 1421 | 1422 | 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: 1423 | 1424 | ```js 1425 | var options = { 1426 | fields: { 1427 | search: { 1428 | factory: t.form.Textbox 1429 | } 1430 | } 1431 | }; 1432 | ``` 1433 | 1434 | 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: 1435 | 1436 | ```js 1437 | var Transformer = t.struct({ 1438 | format: t.Function, // from value to string, it must be idempotent 1439 | parse: t.Function // from string to value 1440 | }); 1441 | ``` 1442 | 1443 | A basic transformer implementation for the search textbox: 1444 | 1445 | ```js 1446 | var listTransformer = { 1447 | format: function (value) { 1448 | return Array.isArray(value) ? value.join(' ') : value; 1449 | }, 1450 | parse: function (str) { 1451 | return str ? str.split(' ') : []; 1452 | } 1453 | }; 1454 | ``` 1455 | 1456 | Now you can handle lists using the transformer option: 1457 | 1458 | ```js 1459 | // example of initial value 1460 | var value = { 1461 | search: ['climbing', 'yosemite'] 1462 | }; 1463 | 1464 | var options = { 1465 | fields: { 1466 | search: { 1467 | factory: t.form.Textbox, // tell tcomb-react-native to use the same component for textboxes 1468 | transformer: listTransformer, 1469 | help: 'Keywords are separated by spaces' 1470 | } 1471 | } 1472 | }; 1473 | ``` 1474 | 1475 | ## Custom factories 1476 | 1477 | You can pack together style, template (and transformers) in a custom component and then you can use it with the `factory` option: 1478 | 1479 | ```js 1480 | var Component = t.form.Component; 1481 | 1482 | // extend the base Component 1483 | class MyComponent extends Component { 1484 | 1485 | // this is the only required method to implement 1486 | getTemplate() { 1487 | // define here your custom template 1488 | return function (locals) { 1489 | 1490 | //return ... jsx ... 1491 | 1492 | }; 1493 | } 1494 | 1495 | // you can optionally override the default getLocals method 1496 | // it will provide the locals param to your template 1497 | getLocals() { 1498 | 1499 | // in locals you'll find the default locals: 1500 | // - path 1501 | // - error 1502 | // - hasError 1503 | // - label 1504 | // - onChange 1505 | // - stylesheet 1506 | var locals = super.getLocals(); 1507 | 1508 | // add here your custom locals 1509 | 1510 | return locals; 1511 | } 1512 | 1513 | 1514 | } 1515 | 1516 | // as example of transformer: this is the default transformer for textboxes 1517 | MyComponent.transformer = { 1518 | format: value => Nil.is(value) ? null : value, 1519 | parse: value => (t.String.is(value) && value.trim() === '') || Nil.is(value) ? null : value 1520 | }; 1521 | 1522 | var Person = t.struct({ 1523 | name: t.String 1524 | }); 1525 | 1526 | var options = { 1527 | fields: { 1528 | name: { 1529 | factory: MyComponent 1530 | } 1531 | } 1532 | }; 1533 | ``` 1534 | 1535 | # Tests 1536 | 1537 | ``` 1538 | npm test 1539 | ``` 1540 | **Note:** If you are using Jest, you will encounter an error which can 1541 | be fixed w/ a small change to the ```package.json```. 1542 | 1543 | The error will look similiar to the following: 1544 | ``` 1545 | Error: Cannot find module './datepicker' from 'index.js' at 1546 | Resolver.resolveModule 1547 | ``` 1548 | 1549 | A completely working example ```jest``` setup is shown below w/ the 1550 | [http://facebook.github.io/jest/docs/api.html#modulefileextensions-array-string](http://facebook.github.io/jest/docs/api.html#modulefileextensions-array-string) 1551 | fix added: 1552 | 1553 | ``` 1554 | "jest": { 1555 | "setupEnvScriptFile": "./node_modules/react-native/jestSupport/env.js", 1556 | "haste": { 1557 | "defaultPlatform": "ios", 1558 | "platforms": [ 1559 | "ios", 1560 | "android" 1561 | ], 1562 | "providesModuleNodeModules": [ 1563 | "react-native" 1564 | ] 1565 | }, 1566 | "testPathIgnorePatterns": [ 1567 | "/node_modules/" 1568 | ], 1569 | "testFileExtensions": [ 1570 | "es6", 1571 | "js" 1572 | ], 1573 | "moduleFileExtensions": [ 1574 | "js", 1575 | "json", 1576 | "es6", 1577 | "ios.js" <<<<<<<<<<<< this needs to be defined! 1578 | ], 1579 | "unmockedModulePathPatterns": [ 1580 | "react", 1581 | "react-addons-test-utils", 1582 | "react-native-router-flux", 1583 | "promise", 1584 | "source-map", 1585 | "key-mirror", 1586 | "immutable", 1587 | "fetch", 1588 | "redux", 1589 | "redux-thunk", 1590 | "fbjs" 1591 | ], 1592 | "collectCoverage": false, 1593 | "verbose": true 1594 | }, 1595 | ``` 1596 | 1597 | # License 1598 | 1599 | [MIT](LICENSE) 1600 | -------------------------------------------------------------------------------- /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 without 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 occures 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.textboxView.normal.marginBottom = 5; 211 | stylesheet.textboxView.error.marginBottom = 5; 212 | 213 | const options = { 214 | stylesheet: stylesheet 215 | }; 216 | ``` 217 | 218 | **Output** 219 | 220 | ![](images/stylesheets/7.png) 221 | 222 | -------------------------------------------------------------------------------- /docs/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/error.png -------------------------------------------------------------------------------- /docs/images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/help.png -------------------------------------------------------------------------------- /docs/images/placeholders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/placeholders.png -------------------------------------------------------------------------------- /docs/images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/result.png -------------------------------------------------------------------------------- /docs/images/stylesheets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/1.png -------------------------------------------------------------------------------- /docs/images/stylesheets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/2.png -------------------------------------------------------------------------------- /docs/images/stylesheets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/3.png -------------------------------------------------------------------------------- /docs/images/stylesheets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/4.png -------------------------------------------------------------------------------- /docs/images/stylesheets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/5.png -------------------------------------------------------------------------------- /docs/images/stylesheets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/6.png -------------------------------------------------------------------------------- /docs/images/stylesheets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/stylesheets/7.png -------------------------------------------------------------------------------- /docs/images/validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/tcomb-form-native/cb70ddf0a2a900094fdf3809ae6dd0afa84b719f/docs/images/validation.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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() === "placeholders") { 156 | label = null; 157 | } else if (Nil.is(label) && this.getAuto() === "labels") { 158 | label = this.getDefaultLabel(); 159 | } else if (label) { 160 | label = 161 | label + 162 | (this.typeInfo.isMaybe 163 | ? this.getI18n().optional 164 | : this.getI18n().required); 165 | } 166 | return label; 167 | } 168 | 169 | getError() { 170 | if (this.hasError()) { 171 | const error = 172 | this.props.options.error || this.typeInfo.getValidationErrorMessage; 173 | if (t.Function.is(error)) { 174 | const validationOptions = this.getValidationOptions(); 175 | return error( 176 | this.getValue(), 177 | validationOptions.path, 178 | validationOptions.context 179 | ); 180 | } 181 | return error; 182 | } 183 | } 184 | 185 | hasError() { 186 | return this.props.options.hasError || this.state.hasError; 187 | } 188 | 189 | getConfig() { 190 | return merge(this.props.ctx.config, this.props.options.config); 191 | } 192 | 193 | getStylesheet() { 194 | return this.props.options.stylesheet || this.props.ctx.stylesheet; 195 | } 196 | 197 | getLocals() { 198 | return { 199 | path: this.props.ctx.path, 200 | error: this.getError(), 201 | hasError: this.hasError(), 202 | label: this.getLabel(), 203 | onChange: this.onChange.bind(this), 204 | config: this.getConfig(), 205 | value: this.state.value, 206 | hidden: this.props.options.hidden, 207 | stylesheet: this.getStylesheet() 208 | }; 209 | } 210 | 211 | render() { 212 | const locals = this.getLocals(); 213 | // getTemplate is the only required implementation when extending Component 214 | t.assert( 215 | t.Function.is(this.getTemplate), 216 | `[${SOURCE}] missing getTemplate method of component ${ 217 | this.constructor.name 218 | }` 219 | ); 220 | const template = this.getTemplate(); 221 | return template(locals); 222 | } 223 | } 224 | 225 | Component.transformer = { 226 | format: value => (Nil.is(value) ? null : value), 227 | parse: value => value 228 | }; 229 | 230 | function toNull(value) { 231 | return (t.String.is(value) && value.trim() === "") || Nil.is(value) 232 | ? null 233 | : value; 234 | } 235 | 236 | function parseNumber(value) { 237 | const n = parseFloat(value); 238 | const isNumeric = value - n + 1 >= 0; 239 | return isNumeric ? n : toNull(value); 240 | } 241 | 242 | class Textbox extends Component { 243 | getTransformer() { 244 | const options = this.props.options; 245 | return options.transformer 246 | ? options.transformer 247 | : this.typeInfo.innerType === t.Number 248 | ? Textbox.numberTransformer 249 | : Textbox.transformer; 250 | } 251 | 252 | getTemplate() { 253 | return this.props.options.template || this.props.ctx.templates.textbox; 254 | } 255 | 256 | getPlaceholder() { 257 | let placeholder = this.props.options.placeholder; 258 | if (Nil.is(placeholder) && this.getAuto() === "placeholders") { 259 | placeholder = this.getDefaultLabel(); 260 | } 261 | return placeholder; 262 | } 263 | 264 | getKeyboardType() { 265 | const keyboardType = this.props.options.keyboardType; 266 | if (t.Nil.is(keyboardType) && this.typeInfo.innerType === t.Number) { 267 | return "numeric"; 268 | } 269 | return keyboardType; 270 | } 271 | 272 | getLocals() { 273 | const locals = super.getLocals(); 274 | locals.placeholder = this.getPlaceholder(); 275 | locals.onChangeNative = this.props.options.onChange; 276 | locals.keyboardType = this.getKeyboardType(); 277 | locals.underlineColorAndroid = 278 | this.props.options.underlineColorAndroid || "transparent"; 279 | [ 280 | "help", 281 | "allowFontScaling", 282 | "autoCapitalize", 283 | "autoCorrect", 284 | "autoFocus", 285 | "blurOnSubmit", 286 | "editable", 287 | "maxLength", 288 | "multiline", 289 | "onBlur", 290 | "onEndEditing", 291 | "onFocus", 292 | "onLayout", 293 | "onSelectionChange", 294 | "onSubmitEditing", 295 | "onContentSizeChange", 296 | "placeholderTextColor", 297 | "secureTextEntry", 298 | "selectTextOnFocus", 299 | "selectionColor", 300 | "numberOfLines", 301 | "clearButtonMode", 302 | "clearTextOnFocus", 303 | "enablesReturnKeyAutomatically", 304 | "keyboardAppearance", 305 | "onKeyPress", 306 | "returnKeyType", 307 | "selectionState", 308 | "testID", 309 | "textContentType" 310 | ].forEach(name => (locals[name] = this.props.options[name])); 311 | 312 | return locals; 313 | } 314 | } 315 | 316 | Textbox.transformer = { 317 | format: value => (Nil.is(value) ? "" : value), 318 | parse: toNull 319 | }; 320 | 321 | Textbox.numberTransformer = { 322 | format: value => (Nil.is(value) ? "" : String(value)), 323 | parse: parseNumber 324 | }; 325 | 326 | class Checkbox extends Component { 327 | getTemplate() { 328 | return this.props.options.template || this.props.ctx.templates.checkbox; 329 | } 330 | 331 | getLocals() { 332 | const locals = super.getLocals(); 333 | locals.label = 334 | this.props.ctx.auto !== "none" 335 | ? locals.label || this.getDefaultLabel() 336 | : null; 337 | ["help", "disabled", "onTintColor", "thumbTintColor", "tintColor", "testID"].forEach( 338 | name => (locals[name] = this.props.options[name]) 339 | ); 340 | 341 | return locals; 342 | } 343 | } 344 | 345 | Checkbox.transformer = { 346 | format: value => (Nil.is(value) ? false : value), 347 | parse: value => value 348 | }; 349 | 350 | class Select extends Component { 351 | getTransformer() { 352 | const options = this.props.options; 353 | if (options.transformer) { 354 | return options.transformer; 355 | } 356 | return Select.transformer(this.getNullOption()); 357 | } 358 | 359 | getTemplate() { 360 | return this.props.options.template || this.props.ctx.templates.select; 361 | } 362 | 363 | getNullOption() { 364 | return this.props.options.nullOption || { value: "", text: "-" }; 365 | } 366 | 367 | getEnum() { 368 | return this.typeInfo.innerType; 369 | } 370 | 371 | getOptions() { 372 | const options = this.props.options; 373 | const items = options.options 374 | ? options.options.slice() 375 | : getOptionsOfEnum(this.getEnum()); 376 | if (options.order) { 377 | items.sort(getComparator(options.order)); 378 | } 379 | const nullOption = this.getNullOption(); 380 | if (options.nullOption !== false) { 381 | items.unshift(nullOption); 382 | } 383 | return items; 384 | } 385 | 386 | getLocals() { 387 | const locals = super.getLocals(); 388 | locals.options = this.getOptions(); 389 | 390 | [ 391 | "help", 392 | "disabled", 393 | "mode", 394 | "prompt", 395 | "itemStyle", 396 | "isCollapsed", 397 | "onCollapseChange", 398 | "testID" 399 | ].forEach(name => (locals[name] = this.props.options[name])); 400 | 401 | return locals; 402 | } 403 | } 404 | 405 | Select.transformer = nullOption => { 406 | return { 407 | format: value => 408 | Nil.is(value) && nullOption ? nullOption.value : String(value), 409 | parse: value => (nullOption && nullOption.value === value ? null : value) 410 | }; 411 | }; 412 | 413 | class DatePicker extends Component { 414 | getTemplate() { 415 | return this.props.options.template || this.props.ctx.templates.datepicker; 416 | } 417 | 418 | getLocals() { 419 | const locals = super.getLocals(); 420 | [ 421 | "help", 422 | "disabled", 423 | "maximumDate", 424 | "minimumDate", 425 | "minuteInterval", 426 | "mode", 427 | "timeZoneOffsetInMinutes", 428 | "onPress" 429 | ].forEach(name => (locals[name] = this.props.options[name])); 430 | 431 | return locals; 432 | } 433 | } 434 | 435 | DatePicker.transformer = { 436 | format: value => (Nil.is(value) ? null : value), 437 | parse: value => value 438 | }; 439 | 440 | class Struct extends Component { 441 | isValueNully() { 442 | return Object.keys(this.refs).every(ref => this.refs[ref].isValueNully()); 443 | } 444 | 445 | removeErrors() { 446 | this.setState({ hasError: false }); 447 | Object.keys(this.refs).forEach(ref => this.refs[ref].removeErrors()); 448 | } 449 | 450 | getValue() { 451 | const value = {}; 452 | for (const ref in this.refs) { 453 | value[ref] = this.refs[ref].getValue(); 454 | } 455 | return this.getTransformer().parse(value); 456 | } 457 | 458 | validate() { 459 | let value = {}; 460 | let errors = []; 461 | let hasError = false; 462 | let result; 463 | 464 | if (this.typeInfo.isMaybe && this.isValueNully()) { 465 | this.removeErrors(); 466 | return new t.ValidationResult({ errors: [], value: null }); 467 | } 468 | 469 | for (const ref in this.refs) { 470 | if (this.refs.hasOwnProperty(ref)) { 471 | result = this.refs[ref].validate(); 472 | errors = errors.concat(result.errors); 473 | value[ref] = result.value; 474 | } 475 | } 476 | 477 | if (errors.length === 0) { 478 | const InnerType = this.typeInfo.innerType; 479 | value = new InnerType(value); 480 | if (this.typeInfo.isSubtype && errors.length === 0) { 481 | result = t.validate( 482 | value, 483 | this.props.type, 484 | this.getValidationOptions() 485 | ); 486 | hasError = !result.isValid(); 487 | errors = errors.concat(result.errors); 488 | } 489 | } 490 | 491 | this.setState({ hasError: hasError }); 492 | return new t.ValidationResult({ errors, value }); 493 | } 494 | 495 | onChange(fieldName, fieldValue, path) { 496 | const value = t.mixin({}, this.state.value); 497 | value[fieldName] = fieldValue; 498 | this.setState({ value }, () => { 499 | this.props.onChange(value, path); 500 | }); 501 | } 502 | 503 | getTemplates() { 504 | return merge(this.props.ctx.templates, this.props.options.templates); 505 | } 506 | 507 | getTemplate() { 508 | return this.props.options.template || this.getTemplates().struct; 509 | } 510 | 511 | getTypeProps() { 512 | return this.typeInfo.innerType.meta.props; 513 | } 514 | 515 | getOrder() { 516 | return this.props.options.order || Object.keys(this.getTypeProps()); 517 | } 518 | 519 | getInputs() { 520 | const { ctx, options } = this.props; 521 | const props = this.getTypeProps(); 522 | const auto = this.getAuto(); 523 | const i18n = this.getI18n(); 524 | const config = this.getConfig(); 525 | const value = this.state.value || {}; 526 | const templates = this.getTemplates(); 527 | const stylesheet = this.getStylesheet(); 528 | const inputs = {}; 529 | for (const prop in props) { 530 | if (props.hasOwnProperty(prop)) { 531 | const type = props[prop]; 532 | const propValue = value[prop]; 533 | const propType = getTypeFromUnion(type, propValue); 534 | const fieldsOptions = options.fields || noobj; 535 | const propOptions = getComponentOptions( 536 | fieldsOptions[prop], 537 | noobj, 538 | propValue, 539 | type 540 | ); 541 | inputs[prop] = React.createElement( 542 | getFormComponent(propType, propOptions), 543 | { 544 | key: prop, 545 | ref: prop, 546 | type: propType, 547 | options: propOptions, 548 | value: value[prop], 549 | onChange: this.onChange.bind(this, prop), 550 | ctx: { 551 | context: ctx.context, 552 | uidGenerator: ctx.uidGenerator, 553 | auto, 554 | config, 555 | label: humanize(prop), 556 | i18n, 557 | stylesheet, 558 | templates, 559 | path: this.props.ctx.path.concat(prop) 560 | } 561 | } 562 | ); 563 | } 564 | } 565 | return inputs; 566 | } 567 | 568 | getLocals() { 569 | const templates = this.getTemplates(); 570 | const locals = super.getLocals(); 571 | locals.order = this.getOrder(); 572 | locals.inputs = this.getInputs(); 573 | locals.template = templates.struct; 574 | return locals; 575 | } 576 | } 577 | 578 | function toSameLength(value, keys, uidGenerator) { 579 | if (value.length === keys.length) { 580 | return keys; 581 | } 582 | const ret = []; 583 | for (let i = 0, len = value.length; i < len; i++) { 584 | ret[i] = keys[i] || uidGenerator.next(); 585 | } 586 | return ret; 587 | } 588 | 589 | export class List extends Component { 590 | constructor(props) { 591 | super(props); 592 | this.state.keys = this.state.value.map(() => props.ctx.uidGenerator.next()); 593 | } 594 | 595 | componentWillReceiveProps(props) { 596 | if (props.type !== this.props.type) { 597 | this.typeInfo = getTypeInfo(props.type); 598 | } 599 | const value = this.getTransformer().format(props.value); 600 | this.setState({ 601 | value, 602 | keys: toSameLength(value, this.state.keys, props.ctx.uidGenerator) 603 | }); 604 | } 605 | 606 | isValueNully() { 607 | return this.state.value.length === 0; 608 | } 609 | 610 | removeErrors() { 611 | this.setState({ hasError: false }); 612 | Object.keys(this.refs).forEach(ref => this.refs[ref].removeErrors()); 613 | } 614 | 615 | getValue() { 616 | const value = []; 617 | for (let i = 0, len = this.state.value.length; i < len; i++) { 618 | if (this.refs.hasOwnProperty(i)) { 619 | value.push(this.refs[i].getValue()); 620 | } 621 | } 622 | return this.getTransformer().parse(value); 623 | } 624 | 625 | validate() { 626 | const value = []; 627 | let errors = []; 628 | let hasError = false; 629 | let result; 630 | 631 | if (this.typeInfo.isMaybe && this.isValueNully()) { 632 | this.removeErrors(); 633 | return new t.ValidationResult({ errors: [], value: null }); 634 | } 635 | 636 | for (let i = 0, len = this.state.value.length; i < len; i++) { 637 | result = this.refs[i].validate(); 638 | errors = errors.concat(result.errors); 639 | value.push(result.value); 640 | } 641 | 642 | // handle subtype 643 | if (this.typeInfo.isSubtype && errors.length === 0) { 644 | result = t.validate(value, this.props.type, this.getValidationOptions()); 645 | hasError = !result.isValid(); 646 | errors = errors.concat(result.errors); 647 | } 648 | 649 | this.setState({ hasError: hasError }); 650 | return new t.ValidationResult({ errors: errors, value: value }); 651 | } 652 | 653 | onChange(value, keys, path, kind) { 654 | const allkeys = toSameLength(value, keys, this.props.ctx.uidGenerator); 655 | this.setState({ value, keys: allkeys, isPristine: false }, () => { 656 | this.props.onChange(value, path, kind); 657 | }); 658 | } 659 | 660 | addItem() { 661 | const value = this.state.value.concat(undefined); 662 | const keys = this.state.keys.concat(this.props.ctx.uidGenerator.next()); 663 | this.onChange( 664 | value, 665 | keys, 666 | this.props.ctx.path.concat(value.length - 1), 667 | "add" 668 | ); 669 | } 670 | 671 | onItemChange(itemIndex, itemValue, path, kind) { 672 | const value = this.state.value.slice(); 673 | value[itemIndex] = itemValue; 674 | this.onChange(value, this.state.keys, path, kind); 675 | } 676 | 677 | removeItem(i) { 678 | const value = this.state.value.slice(); 679 | value.splice(i, 1); 680 | const keys = this.state.keys.slice(); 681 | keys.splice(i, 1); 682 | this.onChange(value, keys, this.props.ctx.path.concat(i), "remove"); 683 | } 684 | 685 | moveUpItem(i) { 686 | if (i > 0) { 687 | this.onChange( 688 | move(this.state.value.slice(), i, i - 1), 689 | move(this.state.keys.slice(), i, i - 1), 690 | this.props.ctx.path.concat(i), 691 | "moveUp" 692 | ); 693 | } 694 | } 695 | 696 | moveDownItem(i) { 697 | if (i < this.state.value.length - 1) { 698 | this.onChange( 699 | move(this.state.value.slice(), i, i + 1), 700 | move(this.state.keys.slice(), i, i + 1), 701 | this.props.ctx.path.concat(i), 702 | "moveDown" 703 | ); 704 | } 705 | } 706 | 707 | getTemplates() { 708 | return merge(this.props.ctx.templates, this.props.options.templates); 709 | } 710 | 711 | getTemplate() { 712 | return this.props.options.template || this.getTemplates().list; 713 | } 714 | 715 | getItems() { 716 | const { options, ctx } = this.props; 717 | const auto = this.getAuto(); 718 | const i18n = this.getI18n(); 719 | const config = this.getConfig(); 720 | const stylesheet = this.getStylesheet(); 721 | const templates = this.getTemplates(); 722 | const value = this.state.value; 723 | return value.map((itemValue, i) => { 724 | const type = this.typeInfo.innerType.meta.type; 725 | const itemType = getTypeFromUnion(type, itemValue); 726 | const itemOptions = getComponentOptions( 727 | options.item, 728 | noobj, 729 | itemValue, 730 | type 731 | ); 732 | const ItemComponent = getFormComponent(itemType, itemOptions); 733 | const buttons = []; 734 | if (!options.disableRemove) { 735 | buttons.push({ 736 | type: "remove", 737 | label: i18n.remove, 738 | click: this.removeItem.bind(this, i) 739 | }); 740 | } 741 | if (!options.disableOrder) { 742 | buttons.push( 743 | { 744 | type: "move-up", 745 | label: i18n.up, 746 | click: this.moveUpItem.bind(this, i) 747 | }, 748 | { 749 | type: "move-down", 750 | label: i18n.down, 751 | click: this.moveDownItem.bind(this, i) 752 | } 753 | ); 754 | } 755 | return { 756 | input: React.createElement(ItemComponent, { 757 | ref: i, 758 | type: itemType, 759 | options: itemOptions, 760 | value: itemValue, 761 | onChange: this.onItemChange.bind(this, i), 762 | ctx: { 763 | context: ctx.context, 764 | uidGenerator: ctx.uidGenerator, 765 | auto, 766 | config, 767 | label: ctx.label ? `${ctx.label}[${i + 1}]` : String(i + 1), 768 | i18n, 769 | stylesheet, 770 | templates, 771 | path: ctx.path.concat(i) 772 | } 773 | }), 774 | key: this.state.keys[i], 775 | buttons: buttons 776 | }; 777 | }); 778 | } 779 | 780 | getLocals() { 781 | const options = this.props.options; 782 | const i18n = this.getI18n(); 783 | const locals = super.getLocals(); 784 | locals.add = options.disableAdd 785 | ? null 786 | : { 787 | type: "add", 788 | label: i18n.add, 789 | click: this.addItem.bind(this) 790 | }; 791 | locals.items = this.getItems(); 792 | locals.className = options.className; 793 | return locals; 794 | } 795 | } 796 | 797 | List.transformer = { 798 | format: value => (Nil.is(value) ? noarr : value), 799 | parse: value => value 800 | }; 801 | 802 | class Form extends React.Component { 803 | pureValidate() { 804 | return this.refs.input.pureValidate(); 805 | } 806 | 807 | validate() { 808 | return this.refs.input.validate(); 809 | } 810 | 811 | getValue() { 812 | const result = this.validate(); 813 | return result.isValid() ? result.value : null; 814 | } 815 | 816 | getComponent(path) { 817 | path = t.String.is(path) ? path.split(".") : path; 818 | return path.reduce((input, name) => input.refs[name], this.refs.input); 819 | } 820 | 821 | getSeed() { 822 | const rii = this._reactInternalInstance; 823 | if (rii) { 824 | if (rii._hostContainerInfo) { 825 | return rii._hostContainerInfo._idCounter; 826 | } 827 | if (rii._nativeContainerInfo) { 828 | return rii._nativeContainerInfo._idCounter; 829 | } 830 | if (rii._rootNodeID) { 831 | return rii._rootNodeID; 832 | } 833 | } 834 | return "0"; 835 | } 836 | 837 | getUIDGenerator() { 838 | this.uidGenerator = this.uidGenerator || new UIDGenerator(this.getSeed()); 839 | return this.uidGenerator; 840 | } 841 | 842 | render() { 843 | const stylesheet = this.props.stylesheet || Form.stylesheet; 844 | const templates = this.props.templates || Form.templates; 845 | const i18n = this.props.i18n || Form.i18n; 846 | 847 | if (process.env.NODE_ENV !== "production") { 848 | t.assert( 849 | t.isType(this.props.type), 850 | `[${SOURCE}] missing required prop type` 851 | ); 852 | t.assert( 853 | t.maybe(t.Object).is(this.props.options) || 854 | t.Function.is(this.props.options) || 855 | t.list(t.maybe(t.Object)).is(this.props.options), 856 | `[${SOURCE}] prop options, if specified, must be an object, a function returning the options or a list of options for unions` 857 | ); 858 | t.assert( 859 | t.Object.is(stylesheet), 860 | `[${SOURCE}] missing stylesheet config` 861 | ); 862 | t.assert(t.Object.is(templates), `[${SOURCE}] missing templates config`); 863 | t.assert(t.Object.is(i18n), `[${SOURCE}] missing i18n config`); 864 | } 865 | 866 | const value = this.props.value; 867 | const type = getTypeFromUnion(this.props.type, value); 868 | const options = getComponentOptions( 869 | this.props.options, 870 | noobj, 871 | value, 872 | this.props.type 873 | ); 874 | 875 | // this is in the render method because I need this._reactInternalInstance._rootNodeID in React ^0.14.0 876 | // and this._reactInternalInstance._nativeContainerInfo._idCounter in React ^15.0.0 877 | const uidGenerator = this.getUIDGenerator(); 878 | 879 | return React.createElement(getFormComponent(type, options), { 880 | ref: "input", 881 | type: type, 882 | options: options, 883 | value: this.props.value, 884 | onChange: this.props.onChange || noop, 885 | ctx: { 886 | context: this.props.context, 887 | uidGenerator, 888 | auto: "labels", 889 | stylesheet, 890 | templates, 891 | i18n, 892 | path: [] 893 | } 894 | }); 895 | } 896 | } 897 | 898 | module.exports = { 899 | getComponent: getFormComponent, 900 | Component, 901 | Textbox, 902 | Checkbox, 903 | Select, 904 | DatePicker, 905 | Struct, 906 | List: List, 907 | Form 908 | }; 909 | -------------------------------------------------------------------------------- /lib/i18n/en.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | optional: " (optional)", 3 | required: "", 4 | add: "Add", 5 | remove: "✘", 6 | up: "↑", 7 | down: "↓" 8 | }; 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | testID={locals.testID} 50 | /> 51 | {help} 52 | {error} 53 | 54 | ); 55 | } 56 | 57 | module.exports = checkbox; 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | this.onValueChange = this._onValueChange.bind(this); 22 | } 23 | 24 | _animatePicker(isCollapsed) { 25 | const locals = this.props.locals; 26 | let animation = Animated.timing; 27 | let animationConfig = { 28 | duration: 200 29 | }; 30 | if (locals.config) { 31 | if (locals.config.animation) { 32 | animation = locals.config.animation; 33 | } 34 | if (locals.config.animationConfig) { 35 | animationConfig = locals.config.animationConfig; 36 | } 37 | } 38 | animation( 39 | this.state.height, 40 | Object.assign( 41 | { 42 | toValue: isCollapsed ? 0 : UIPICKER_HEIGHT 43 | }, 44 | animationConfig 45 | ) 46 | ).start(); 47 | } 48 | 49 | _togglePicker() { 50 | this.setState({ isCollapsed: !this.state.isCollapsed }, () => { 51 | this.animatePicker(this.state.isCollapsed); 52 | if (typeof this.props.locals.onCollapseChange === "function") { 53 | this.props.locals.onCollapseChange(this.state.isCollapsed); 54 | } 55 | }); 56 | } 57 | 58 | _onValueChange(val) { 59 | const { onChange, value } = this.props.locals; 60 | if (val !== value) { 61 | this.togglePicker(); 62 | onChange(val); 63 | } 64 | } 65 | 66 | render() { 67 | const locals = this.props.locals; 68 | const { stylesheet } = locals; 69 | let pickerContainer = stylesheet.pickerContainer.normal; 70 | let pickerContainerOpen = stylesheet.pickerContainer.open; 71 | let selectStyle = stylesheet.select.normal; 72 | let touchableStyle = stylesheet.pickerTouchable.normal; 73 | let touchableStyleActive = stylesheet.pickerTouchable.active; 74 | let pickerValue = stylesheet.pickerValue.normal; 75 | if (locals.hasError) { 76 | pickerContainer = stylesheet.pickerContainer.error; 77 | selectStyle = stylesheet.select.error; 78 | touchableStyle = stylesheet.pickerTouchable.error; 79 | pickerValue = stylesheet.pickerValue.error; 80 | } 81 | 82 | if (locals.disabled) { 83 | touchableStyle = stylesheet.pickerTouchable.notEditable; 84 | } 85 | 86 | const options = locals.options.map(({ value, text }) => ( 87 | 88 | )); 89 | const selectedOption = locals.options.find( 90 | option => option.value === locals.value 91 | ); 92 | 93 | const height = this.state.isCollapsed ? 0 : UIPICKER_HEIGHT; 94 | 95 | return ( 96 | 102 | 110 | {selectedOption.text} 111 | 112 | 115 | 127 | {options} 128 | 129 | 130 | 131 | ); 132 | } 133 | } 134 | 135 | CollapsiblePickerIOS.propTypes = { 136 | locals: PropTypes.object.isRequired 137 | }; 138 | 139 | function select(locals) { 140 | if (locals.hidden) { 141 | return null; 142 | } 143 | 144 | const stylesheet = locals.stylesheet; 145 | let formGroupStyle = stylesheet.formGroup.normal; 146 | let controlLabelStyle = stylesheet.controlLabel.normal; 147 | let selectStyle = stylesheet.select.normal; 148 | let helpBlockStyle = stylesheet.helpBlock.normal; 149 | let errorBlockStyle = stylesheet.errorBlock; 150 | 151 | if (locals.hasError) { 152 | formGroupStyle = stylesheet.formGroup.error; 153 | controlLabelStyle = stylesheet.controlLabel.error; 154 | selectStyle = stylesheet.select.error; 155 | helpBlockStyle = stylesheet.helpBlock.error; 156 | } 157 | 158 | const label = locals.label ? ( 159 | {locals.label} 160 | ) : null; 161 | const help = locals.help ? ( 162 | {locals.help} 163 | ) : null; 164 | const error = 165 | locals.hasError && locals.error ? ( 166 | 167 | {locals.error} 168 | 169 | ) : null; 170 | 171 | var options = locals.options.map(({ value, text }) => ( 172 | 173 | )); 174 | 175 | return ( 176 | 177 | {label} 178 | 179 | {help} 180 | {error} 181 | 182 | ); 183 | } 184 | 185 | module.exports = select; 186 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | testID={locals.testID} 84 | textContentType={locals.textContentType} 85 | /> 86 | 87 | {help} 88 | {error} 89 | 90 | ); 91 | } 92 | 93 | module.exports = textbox; 94 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcomb-form-native", 3 | "version": "0.6.20", 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": ["tcomb", "form", "forms", "react", "react-native", "react-component"] 40 | } 41 | -------------------------------------------------------------------------------- /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(4); 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 | tape.strictEqual( 78 | new Textbox({ 79 | type: t.maybe(t.String), 80 | options: { label: "mylabel" }, 81 | ctx: ctx 82 | }).getLocals().label, 83 | "mylabel (optional)", 84 | "should handle `label` option with optional types" 85 | ); 86 | }); 87 | 88 | test("Textbox:placeholder", function(tape) { 89 | tape.plan(6); 90 | 91 | tape.strictEqual( 92 | new Textbox({ 93 | type: t.String, 94 | options: {}, 95 | ctx: ctx 96 | }).getLocals().placeholder, 97 | undefined, 98 | "default placeholder should be undefined" 99 | ); 100 | 101 | tape.strictEqual( 102 | new Textbox({ 103 | type: t.String, 104 | options: { placeholder: "myplaceholder" }, 105 | ctx: ctx 106 | }).getLocals().placeholder, 107 | "myplaceholder", 108 | "should handle placeholder option" 109 | ); 110 | 111 | tape.strictEqual( 112 | new Textbox({ 113 | type: t.String, 114 | options: { label: "mylabel", placeholder: "myplaceholder" }, 115 | ctx: ctx 116 | }).getLocals().placeholder, 117 | "myplaceholder", 118 | "should handle placeholder option even if a label is specified" 119 | ); 120 | 121 | tape.strictEqual( 122 | new Textbox({ 123 | type: t.String, 124 | options: {}, 125 | ctx: ctxPlaceholders 126 | }).getLocals().placeholder, 127 | "ctxDefaultLabel", 128 | "should have a default placeholder if auto = placeholders" 129 | ); 130 | 131 | tape.strictEqual( 132 | new Textbox({ 133 | type: t.maybe(t.String), 134 | options: {}, 135 | ctx: ctxPlaceholders 136 | }).getLocals().placeholder, 137 | "ctxDefaultLabel (optional)", 138 | "should handle optional types if auto = placeholders" 139 | ); 140 | 141 | tape.strictEqual( 142 | new Textbox({ 143 | type: t.String, 144 | options: { placeholder: "myplaceholder" }, 145 | ctx: ctxNone 146 | }).getLocals().placeholder, 147 | "myplaceholder", 148 | "should handle placeholder option even if auto === none" 149 | ); 150 | }); 151 | 152 | test("Textbox:editable", function(tape) { 153 | tape.plan(3); 154 | 155 | tape.strictEqual( 156 | new Textbox({ 157 | type: t.String, 158 | options: {}, 159 | ctx: ctx 160 | }).getLocals().editable, 161 | undefined, 162 | "default editable should be undefined" 163 | ); 164 | 165 | tape.strictEqual( 166 | new Textbox({ 167 | type: t.String, 168 | options: { editable: true }, 169 | ctx: ctx 170 | }).getLocals().editable, 171 | true, 172 | "should handle editable = true" 173 | ); 174 | 175 | tape.strictEqual( 176 | new Textbox({ 177 | type: t.String, 178 | options: { editable: false }, 179 | ctx: ctx 180 | }).getLocals().editable, 181 | false, 182 | "should handle editable = false" 183 | ); 184 | }); 185 | 186 | test("Textbox:help", function(tape) { 187 | tape.plan(1); 188 | 189 | tape.strictEqual( 190 | new Textbox({ 191 | type: t.String, 192 | options: { help: "myhelp" }, 193 | ctx: ctx 194 | }).getLocals().help, 195 | "myhelp", 196 | "should handle help option" 197 | ); 198 | }); 199 | 200 | test("Textbox:value", function(tape) { 201 | tape.plan(3); 202 | 203 | tape.strictEqual( 204 | new Textbox({ 205 | type: t.String, 206 | options: {}, 207 | ctx: ctx 208 | }).getLocals().value, 209 | "", 210 | "default value should be empty string" 211 | ); 212 | 213 | tape.strictEqual( 214 | new Textbox({ 215 | type: t.String, 216 | options: {}, 217 | ctx: ctx, 218 | value: "a" 219 | }).getLocals().value, 220 | "a", 221 | "should handle value option" 222 | ); 223 | 224 | tape.strictEqual( 225 | new Textbox({ 226 | type: t.Number, 227 | options: {}, 228 | ctx: ctx, 229 | value: 1.1 230 | }).getLocals().value, 231 | "1.1", 232 | "should handle numeric values" 233 | ); 234 | }); 235 | 236 | test("Textbox:transformer", function(tape) { 237 | tape.plan(2); 238 | 239 | var transformer = { 240 | format: function(value) { 241 | return Array.isArray(value) ? value.join(" ") : value; 242 | }, 243 | parse: function(value) { 244 | return value.split(" "); 245 | } 246 | }; 247 | 248 | tape.strictEqual( 249 | new Textbox({ 250 | type: t.String, 251 | options: { transformer: transformer }, 252 | ctx: ctx, 253 | value: ["a", "b"] 254 | }).getLocals().value, 255 | "a b", 256 | "should handle transformer option (format)" 257 | ); 258 | 259 | tape.deepEqual( 260 | new Textbox({ 261 | type: t.String, 262 | options: { transformer: transformer }, 263 | ctx: ctx, 264 | value: ["a", "b"] 265 | }).pureValidate().value, 266 | ["a", "b"], 267 | "should handle transformer option (parse)" 268 | ); 269 | }); 270 | 271 | test("Textbox:hasError", function(tape) { 272 | tape.plan(4); 273 | 274 | tape.strictEqual( 275 | new Textbox({ 276 | type: t.String, 277 | options: {}, 278 | ctx: ctx 279 | }).getLocals().hasError, 280 | false, 281 | "default hasError should be false" 282 | ); 283 | 284 | tape.strictEqual( 285 | new Textbox({ 286 | type: t.String, 287 | options: { hasError: true }, 288 | ctx: ctx 289 | }).getLocals().hasError, 290 | true, 291 | "should handle hasError option" 292 | ); 293 | 294 | var textbox = new Textbox({ 295 | type: t.String, 296 | options: {}, 297 | ctx: ctx 298 | }); 299 | 300 | textbox.pureValidate(); 301 | 302 | tape.strictEqual( 303 | textbox.getLocals().hasError, 304 | false, 305 | "after a validation error hasError should be true" 306 | ); 307 | 308 | textbox = new Textbox({ 309 | type: t.String, 310 | options: {}, 311 | ctx: ctx, 312 | value: "a" 313 | }); 314 | 315 | textbox.pureValidate(); 316 | 317 | tape.strictEqual( 318 | textbox.getLocals().hasError, 319 | false, 320 | "after a validation success hasError should be false" 321 | ); 322 | }); 323 | 324 | test("Textbox:error", function(tape) { 325 | tape.plan(3); 326 | 327 | tape.strictEqual( 328 | new Textbox({ 329 | type: t.String, 330 | options: {}, 331 | ctx: ctx 332 | }).getLocals().error, 333 | undefined, 334 | "default error should be undefined" 335 | ); 336 | 337 | tape.strictEqual( 338 | new Textbox({ 339 | type: t.String, 340 | options: { error: "myerror", hasError: true }, 341 | ctx: ctx 342 | }).getLocals().error, 343 | "myerror", 344 | "should handle error option" 345 | ); 346 | 347 | tape.strictEqual( 348 | new Textbox({ 349 | type: t.String, 350 | options: { 351 | error: function(value) { 352 | return "error: " + value; 353 | }, 354 | hasError: true 355 | }, 356 | ctx: ctx, 357 | value: "a" 358 | }).getLocals().error, 359 | "error: a", 360 | "should handle error option as a function" 361 | ); 362 | }); 363 | 364 | test("Textbox:template", function(tape) { 365 | tape.plan(2); 366 | 367 | tape.strictEqual( 368 | new Textbox({ 369 | type: t.String, 370 | options: {}, 371 | ctx: ctx 372 | }).getTemplate(), 373 | bootstrap.textbox, 374 | "default template should be bootstrap.textbox" 375 | ); 376 | 377 | var template = function() {}; 378 | 379 | tape.strictEqual( 380 | new Textbox({ 381 | type: t.String, 382 | options: { template: template }, 383 | ctx: ctx 384 | }).getTemplate(), 385 | template, 386 | "should handle template option" 387 | ); 388 | }); 389 | 390 | var Select = core.Select; 391 | var Country = t.enums({ 392 | IT: "Italy", 393 | FR: "France", 394 | US: "United States" 395 | }); 396 | 397 | test("Select:label", function(tape) { 398 | tape.plan(3); 399 | 400 | tape.strictEqual( 401 | new Select({ 402 | type: Country, 403 | options: {}, 404 | ctx: ctx 405 | }).getLocals().label, 406 | "ctxDefaultLabel", 407 | "should have a default label" 408 | ); 409 | 410 | tape.strictEqual( 411 | new Select({ 412 | type: Country, 413 | options: { label: "mylabel" }, 414 | ctx: ctx 415 | }).getLocals().label, 416 | "mylabel", 417 | "should handle `label` option" 418 | ); 419 | 420 | tape.strictEqual( 421 | new Select({ 422 | type: t.maybe(Country), 423 | options: {}, 424 | ctx: ctx 425 | }).getLocals().label, 426 | "ctxDefaultLabel (optional)", 427 | "should handle optional types" 428 | ); 429 | }); 430 | 431 | test("Select:help", function(tape) { 432 | tape.plan(1); 433 | 434 | tape.strictEqual( 435 | new Select({ 436 | type: Country, 437 | options: { help: "myhelp" }, 438 | ctx: ctx 439 | }).getLocals().help, 440 | "myhelp", 441 | "should handle help option" 442 | ); 443 | }); 444 | 445 | test("Select:value", function(tape) { 446 | tape.plan(2); 447 | 448 | tape.strictEqual( 449 | new Select({ 450 | type: Country, 451 | options: {}, 452 | ctx: ctx 453 | }).getLocals().value, 454 | "", 455 | "default value should be nullOption.value" 456 | ); 457 | 458 | tape.strictEqual( 459 | new Select({ 460 | type: Country, 461 | options: {}, 462 | ctx: ctx, 463 | value: "a" 464 | }).getLocals().value, 465 | "a", 466 | "should handle value option" 467 | ); 468 | }); 469 | 470 | test("Select:transformer", function(tape) { 471 | tape.plan(2); 472 | 473 | var transformer = { 474 | format: function(value) { 475 | return t.String.is(value) ? value : value === true ? "1" : "0"; 476 | }, 477 | parse: function(value) { 478 | return value === "1"; 479 | } 480 | }; 481 | 482 | tape.strictEqual( 483 | new Select({ 484 | type: t.maybe(t.Boolean), 485 | options: { 486 | transformer: transformer, 487 | options: [{ value: "0", text: "No" }, { value: "1", text: "Yes" }] 488 | }, 489 | ctx: ctx, 490 | value: true 491 | }).getLocals().value, 492 | "1", 493 | "should handle transformer option (format)" 494 | ); 495 | 496 | tape.deepEqual( 497 | new Select({ 498 | type: t.maybe(t.Boolean), 499 | options: { 500 | transformer: transformer, 501 | options: [{ value: "0", text: "No" }, { value: "1", text: "Yes" }] 502 | }, 503 | ctx: ctx, 504 | value: true 505 | }).pureValidate().value, 506 | true, 507 | "should handle transformer option (parse)" 508 | ); 509 | }); 510 | 511 | test("Select:hasError", function(tape) { 512 | tape.plan(4); 513 | 514 | tape.strictEqual( 515 | new Select({ 516 | type: Country, 517 | options: {}, 518 | ctx: ctx 519 | }).getLocals().hasError, 520 | false, 521 | "default hasError should be false" 522 | ); 523 | 524 | tape.strictEqual( 525 | new Select({ 526 | type: Country, 527 | options: { hasError: true }, 528 | ctx: ctx 529 | }).getLocals().hasError, 530 | true, 531 | "should handle hasError option" 532 | ); 533 | 534 | var select = new Select({ 535 | type: Country, 536 | options: {}, 537 | ctx: ctx 538 | }); 539 | 540 | select.pureValidate(); 541 | 542 | tape.strictEqual( 543 | select.getLocals().hasError, 544 | false, 545 | "after a validation error hasError should be true" 546 | ); 547 | 548 | select = new Select({ 549 | type: Country, 550 | options: {}, 551 | ctx: ctx, 552 | value: "IT" 553 | }); 554 | 555 | select.pureValidate(); 556 | 557 | tape.strictEqual( 558 | select.getLocals().hasError, 559 | false, 560 | "after a validation success hasError should be false" 561 | ); 562 | }); 563 | 564 | test("Select:error", function(tape) { 565 | tape.plan(3); 566 | 567 | tape.strictEqual( 568 | new Select({ 569 | type: Country, 570 | options: {}, 571 | ctx: ctx 572 | }).getLocals().error, 573 | undefined, 574 | "default error should be undefined" 575 | ); 576 | 577 | tape.strictEqual( 578 | new Select({ 579 | type: Country, 580 | options: { error: "myerror", hasError: true }, 581 | ctx: ctx 582 | }).getLocals().error, 583 | "myerror", 584 | "should handle error option" 585 | ); 586 | 587 | tape.strictEqual( 588 | new Select({ 589 | type: Country, 590 | options: { 591 | error: function(value) { 592 | return "error: " + value; 593 | }, 594 | hasError: true 595 | }, 596 | ctx: ctx, 597 | value: "a" 598 | }).getLocals().error, 599 | "error: a", 600 | "should handle error option as a function" 601 | ); 602 | }); 603 | 604 | test("Select:template", function(tape) { 605 | tape.plan(2); 606 | 607 | tape.strictEqual( 608 | new Select({ 609 | type: Country, 610 | options: {}, 611 | ctx: ctx 612 | }).getTemplate(), 613 | bootstrap.select, 614 | "default template should be bootstrap.select" 615 | ); 616 | 617 | var template = function() {}; 618 | 619 | tape.strictEqual( 620 | new Select({ 621 | type: Country, 622 | options: { template: template }, 623 | ctx: ctx 624 | }).getTemplate(), 625 | template, 626 | "should handle template option" 627 | ); 628 | }); 629 | 630 | test("Select:options", function(tape) { 631 | tape.plan(1); 632 | 633 | tape.deepEqual( 634 | new Select({ 635 | type: Country, 636 | options: { 637 | options: [ 638 | { value: "IT", text: "Italia" }, 639 | { value: "US", text: "Stati Uniti" } 640 | ] 641 | }, 642 | ctx: ctx 643 | }).getLocals().options, 644 | [ 645 | { text: "-", value: "" }, 646 | { text: "Italia", value: "IT" }, 647 | { text: "Stati Uniti", value: "US" } 648 | ], 649 | "should handle options option" 650 | ); 651 | }); 652 | 653 | test("Select:order", function(tape) { 654 | tape.plan(2); 655 | 656 | tape.deepEqual( 657 | new Select({ 658 | type: Country, 659 | options: { order: "asc" }, 660 | ctx: ctx 661 | }).getLocals().options, 662 | [ 663 | { text: "-", value: "" }, 664 | { text: "France", value: "FR" }, 665 | { text: "Italy", value: "IT" }, 666 | { text: "United States", value: "US" } 667 | ], 668 | "should handle order = asc option" 669 | ); 670 | 671 | tape.deepEqual( 672 | new Select({ 673 | type: Country, 674 | options: { order: "desc" }, 675 | ctx: ctx 676 | }).getLocals().options, 677 | [ 678 | { text: "-", value: "" }, 679 | { text: "United States", value: "US" }, 680 | { text: "Italy", value: "IT" }, 681 | { text: "France", value: "FR" } 682 | ], 683 | "should handle order = desc option" 684 | ); 685 | }); 686 | 687 | test("Select:nullOption", function(tape) { 688 | tape.plan(2); 689 | 690 | tape.deepEqual( 691 | new Select({ 692 | type: Country, 693 | options: { 694 | nullOption: { value: "", text: "Select a country" } 695 | }, 696 | ctx: ctx 697 | }).getLocals().options, 698 | [ 699 | { value: "", text: "Select a country" }, 700 | { text: "Italy", value: "IT" }, 701 | { text: "France", value: "FR" }, 702 | { text: "United States", value: "US" } 703 | ], 704 | "should handle nullOption option" 705 | ); 706 | 707 | tape.deepEqual( 708 | new Select({ 709 | type: Country, 710 | options: { 711 | nullOption: false 712 | }, 713 | ctx: ctx, 714 | value: "US" 715 | }).getLocals().options, 716 | [ 717 | { text: "Italy", value: "IT" }, 718 | { text: "France", value: "FR" }, 719 | { text: "United States", value: "US" } 720 | ], 721 | "should skip the nullOption if nullOption = false" 722 | ); 723 | }); 724 | 725 | test("Select:isCollapsed:true", function(tape) { 726 | tape.plan(1); 727 | 728 | tape.strictEqual( 729 | new Select({ 730 | type: Country, 731 | options: { isCollapsed: true }, 732 | ctx: ctx 733 | }).getLocals().isCollapsed, 734 | true, 735 | "should handle help option" 736 | ); 737 | }); 738 | 739 | test("Select:isCollapsed:false", function(tape) { 740 | tape.plan(1); 741 | 742 | tape.strictEqual( 743 | new Select({ 744 | type: Country, 745 | options: { isCollapsed: false }, 746 | ctx: ctx 747 | }).getLocals().isCollapsed, 748 | false, 749 | "should handle help option" 750 | ); 751 | }); 752 | 753 | test("Select:onCollapse", function(tape) { 754 | tape.plan(1); 755 | const onCollapsFunc = () => {}; 756 | tape.strictEqual( 757 | new Select({ 758 | type: Country, 759 | options: { onCollapseChange: onCollapsFunc }, 760 | ctx: ctx 761 | }).getLocals().onCollapseChange, 762 | onCollapsFunc, 763 | "should handle help option" 764 | ); 765 | }); 766 | 767 | var Checkbox = core.Checkbox; 768 | 769 | test("Checkbox:label", function(tape) { 770 | tape.plan(3); 771 | 772 | tape.strictEqual( 773 | new Checkbox({ 774 | type: t.Boolean, 775 | options: {}, 776 | ctx: ctx 777 | }).getLocals().label, 778 | "ctxDefaultLabel", 779 | "should have a default label" 780 | ); 781 | 782 | tape.strictEqual( 783 | new Checkbox({ 784 | type: t.Boolean, 785 | options: { label: "mylabel" }, 786 | ctx: ctx 787 | }).getLocals().label, 788 | "mylabel", 789 | "should handle `label` option" 790 | ); 791 | 792 | tape.strictEqual( 793 | new Checkbox({ 794 | type: t.Boolean, 795 | options: {}, 796 | ctx: ctxNone 797 | }).getLocals().label, 798 | null, 799 | "should have null `label` when auto `none`" 800 | ); 801 | }); 802 | 803 | test("Checkbox:help", function(tape) { 804 | tape.plan(1); 805 | 806 | tape.strictEqual( 807 | new Checkbox({ 808 | type: t.Boolean, 809 | options: { help: "myhelp" }, 810 | ctx: ctx 811 | }).getLocals().help, 812 | "myhelp", 813 | "should handle help option" 814 | ); 815 | }); 816 | 817 | test("Checkbox:value", function(tape) { 818 | tape.plan(2); 819 | 820 | tape.strictEqual( 821 | new Checkbox({ 822 | type: t.Boolean, 823 | options: {}, 824 | ctx: ctx 825 | }).getLocals().value, 826 | false, 827 | "default value should be false" 828 | ); 829 | 830 | tape.strictEqual( 831 | new Checkbox({ 832 | type: t.Boolean, 833 | options: {}, 834 | ctx: ctx, 835 | value: true 836 | }).getLocals().value, 837 | true, 838 | "should handle value option" 839 | ); 840 | }); 841 | 842 | test("Checkbox:transformer", function(tape) { 843 | tape.plan(2); 844 | 845 | var transformer = { 846 | format: function(value) { 847 | return t.String.is(value) ? value : value === true ? "1" : "0"; 848 | }, 849 | parse: function(value) { 850 | return value === "1"; 851 | } 852 | }; 853 | 854 | tape.strictEqual( 855 | new Checkbox({ 856 | type: t.Boolean, 857 | options: { transformer: transformer }, 858 | ctx: ctx, 859 | value: true 860 | }).getLocals().value, 861 | "1", 862 | "should handle transformer option (format)" 863 | ); 864 | 865 | tape.deepEqual( 866 | new Checkbox({ 867 | type: t.Boolean, 868 | options: { transformer: transformer }, 869 | ctx: ctx, 870 | value: true 871 | }).pureValidate().value, 872 | true, 873 | "should handle transformer option (parse)" 874 | ); 875 | }); 876 | 877 | test("Checkbox:hasError", function(tape) { 878 | tape.plan(4); 879 | 880 | var True = t.subtype(t.Boolean, function(value) { 881 | return value === true; 882 | }); 883 | 884 | tape.strictEqual( 885 | new Checkbox({ 886 | type: True, 887 | options: {}, 888 | ctx: ctx 889 | }).getLocals().hasError, 890 | false, 891 | "default hasError should be false" 892 | ); 893 | 894 | tape.strictEqual( 895 | new Checkbox({ 896 | type: True, 897 | options: { hasError: true }, 898 | ctx: ctx 899 | }).getLocals().hasError, 900 | true, 901 | "should handle hasError option" 902 | ); 903 | 904 | var checkbox = new Checkbox({ 905 | type: True, 906 | options: {}, 907 | ctx: ctx 908 | }); 909 | 910 | checkbox.pureValidate(); 911 | 912 | tape.strictEqual( 913 | checkbox.getLocals().hasError, 914 | false, 915 | "after a validation error hasError should be true" 916 | ); 917 | 918 | checkbox = new Checkbox({ 919 | type: True, 920 | options: {}, 921 | ctx: ctx, 922 | value: true 923 | }); 924 | 925 | checkbox.pureValidate(); 926 | 927 | tape.strictEqual( 928 | checkbox.getLocals().hasError, 929 | false, 930 | "after a validation success hasError should be false" 931 | ); 932 | }); 933 | 934 | test("Checkbox:error", function(tape) { 935 | tape.plan(3); 936 | 937 | tape.strictEqual( 938 | new Checkbox({ 939 | type: t.Boolean, 940 | options: {}, 941 | ctx: ctx 942 | }).getLocals().error, 943 | undefined, 944 | "default error should be undefined" 945 | ); 946 | 947 | tape.strictEqual( 948 | new Checkbox({ 949 | type: t.Boolean, 950 | options: { error: "myerror", hasError: true }, 951 | ctx: ctx 952 | }).getLocals().error, 953 | "myerror", 954 | "should handle error option" 955 | ); 956 | 957 | tape.strictEqual( 958 | new Checkbox({ 959 | type: t.Boolean, 960 | options: { 961 | error: function(value) { 962 | return "error: " + value; 963 | }, 964 | hasError: true 965 | }, 966 | ctx: ctx, 967 | value: "a" 968 | }).getLocals().error, 969 | "error: a", 970 | "should handle error option as a function" 971 | ); 972 | }); 973 | 974 | test("Checkbox:template", function(tape) { 975 | tape.plan(2); 976 | 977 | tape.strictEqual( 978 | new Checkbox({ 979 | type: t.Boolean, 980 | options: {}, 981 | ctx: ctx 982 | }).getTemplate(), 983 | bootstrap.checkbox, 984 | "default template should be bootstrap.checkbox" 985 | ); 986 | 987 | var template = function() {}; 988 | 989 | tape.strictEqual( 990 | new Checkbox({ 991 | type: t.Boolean, 992 | options: { template: template }, 993 | ctx: ctx 994 | }).getTemplate(), 995 | template, 996 | "should handle template option" 997 | ); 998 | }); 999 | 1000 | var DatePicker = core.DatePicker; 1001 | var date = new Date(1973, 10, 30); 1002 | 1003 | test("DatePicker:label", function(tape) { 1004 | tape.plan(2); 1005 | 1006 | tape.strictEqual( 1007 | new DatePicker({ 1008 | type: t.Date, 1009 | options: {}, 1010 | ctx: ctx, 1011 | value: date 1012 | }).getLocals().label, 1013 | "ctxDefaultLabel", 1014 | "should have a default label" 1015 | ); 1016 | 1017 | tape.strictEqual( 1018 | new DatePicker({ 1019 | type: t.Date, 1020 | options: { label: "mylabel" }, 1021 | ctx: ctx, 1022 | value: date 1023 | }).getLocals().label, 1024 | "mylabel", 1025 | "should handle `label` option" 1026 | ); 1027 | }); 1028 | 1029 | test("DatePicker:help", function(tape) { 1030 | tape.plan(1); 1031 | 1032 | tape.strictEqual( 1033 | new DatePicker({ 1034 | type: t.Date, 1035 | options: { help: "myhelp" }, 1036 | ctx: ctx, 1037 | value: date 1038 | }).getLocals().help, 1039 | "myhelp", 1040 | "should handle help option" 1041 | ); 1042 | }); 1043 | 1044 | test("DatePicker:value", function(tape) { 1045 | tape.plan(1); 1046 | 1047 | tape.strictEqual( 1048 | new DatePicker({ 1049 | type: t.Date, 1050 | options: {}, 1051 | ctx: ctx, 1052 | value: date 1053 | }).getLocals().value, 1054 | date, 1055 | "should handle value option" 1056 | ); 1057 | }); 1058 | 1059 | test("DatePicker:transformer", function(tape) { 1060 | tape.plan(2); 1061 | 1062 | var transformer = { 1063 | format: function(value) { 1064 | return Array.isArray(value) 1065 | ? value 1066 | : [value.getFullYear(), value.getMonth(), value.getDate()]; 1067 | }, 1068 | parse: function(value) { 1069 | return new Date(value[0], value[1], value[2]); 1070 | } 1071 | }; 1072 | 1073 | tape.deepEqual( 1074 | new DatePicker({ 1075 | type: t.String, 1076 | options: { transformer: transformer }, 1077 | ctx: ctx, 1078 | value: date 1079 | }).getLocals().value, 1080 | [1973, 10, 30], 1081 | "should handle transformer option (format)" 1082 | ); 1083 | 1084 | tape.deepEqual( 1085 | transformer.format( 1086 | new DatePicker({ 1087 | type: t.Date, 1088 | options: { transformer: transformer }, 1089 | ctx: ctx, 1090 | value: [1973, 10, 30] 1091 | }).pureValidate().value 1092 | ), 1093 | [1973, 10, 30], 1094 | "should handle transformer option (parse)" 1095 | ); 1096 | }); 1097 | 1098 | test("DatePicker:hasError", function(tape) { 1099 | tape.plan(4); 1100 | 1101 | tape.strictEqual( 1102 | new DatePicker({ 1103 | type: t.Date, 1104 | options: {}, 1105 | ctx: ctx, 1106 | value: date 1107 | }).getLocals().hasError, 1108 | false, 1109 | "default hasError should be false" 1110 | ); 1111 | 1112 | tape.strictEqual( 1113 | new DatePicker({ 1114 | type: t.Date, 1115 | options: { hasError: true }, 1116 | ctx: ctx, 1117 | value: date 1118 | }).getLocals().hasError, 1119 | true, 1120 | "should handle hasError option" 1121 | ); 1122 | 1123 | var datePicker = new DatePicker({ 1124 | type: t.Date, 1125 | options: {}, 1126 | ctx: ctx, 1127 | value: date 1128 | }); 1129 | 1130 | datePicker.pureValidate(); 1131 | 1132 | tape.strictEqual( 1133 | datePicker.getLocals().hasError, 1134 | false, 1135 | "after a validation error hasError should be true" 1136 | ); 1137 | 1138 | datePicker = new DatePicker({ 1139 | type: t.Date, 1140 | options: {}, 1141 | ctx: ctx, 1142 | value: date 1143 | }); 1144 | 1145 | datePicker.pureValidate(); 1146 | 1147 | tape.strictEqual( 1148 | datePicker.getLocals().hasError, 1149 | false, 1150 | "after a validation success hasError should be false" 1151 | ); 1152 | }); 1153 | 1154 | test("DatePicker:error", function(tape) { 1155 | tape.plan(3); 1156 | 1157 | tape.strictEqual( 1158 | new DatePicker({ 1159 | type: t.Date, 1160 | options: {}, 1161 | ctx: ctx, 1162 | value: date 1163 | }).getLocals().error, 1164 | undefined, 1165 | "default error should be undefined" 1166 | ); 1167 | 1168 | tape.strictEqual( 1169 | new DatePicker({ 1170 | type: t.Date, 1171 | options: { error: "myerror", hasError: true }, 1172 | ctx: ctx, 1173 | value: date 1174 | }).getLocals().error, 1175 | "myerror", 1176 | "should handle error option" 1177 | ); 1178 | 1179 | tape.strictEqual( 1180 | new DatePicker({ 1181 | type: t.Date, 1182 | options: { 1183 | error: function(value) { 1184 | return "error: " + value.getFullYear(); 1185 | }, 1186 | hasError: true 1187 | }, 1188 | ctx: ctx, 1189 | value: date 1190 | }).getLocals().error, 1191 | "error: 1973", 1192 | "should handle error option as a function" 1193 | ); 1194 | }); 1195 | 1196 | test("DatePicker:template", function(tape) { 1197 | tape.plan(2); 1198 | 1199 | tape.strictEqual( 1200 | new DatePicker({ 1201 | type: t.Date, 1202 | options: {}, 1203 | ctx: ctx, 1204 | value: date 1205 | }).getTemplate(), 1206 | bootstrap.datepicker, 1207 | "default template should be bootstrap.datepicker" 1208 | ); 1209 | 1210 | var template = function() {}; 1211 | 1212 | tape.strictEqual( 1213 | new DatePicker({ 1214 | type: t.Date, 1215 | options: { template: template }, 1216 | ctx: ctx, 1217 | value: date 1218 | }).getTemplate(), 1219 | template, 1220 | "should handle template option" 1221 | ); 1222 | }); 1223 | 1224 | var List = core.List; 1225 | 1226 | test("List:should support unions", assert => { 1227 | assert.plan(2); 1228 | 1229 | const AccountType = t.enums.of(["type 1", "type 2", "other"], "AccountType"); 1230 | 1231 | const KnownAccount = t.struct( 1232 | { 1233 | type: AccountType 1234 | }, 1235 | "KnownAccount" 1236 | ); 1237 | 1238 | const UnknownAccount = KnownAccount.extend( 1239 | { 1240 | label: t.String 1241 | }, 1242 | "UnknownAccount" 1243 | ); 1244 | 1245 | const Account = t.union([KnownAccount, UnknownAccount], "Account"); 1246 | 1247 | Account.dispatch = value => 1248 | value && value.type === "other" ? UnknownAccount : KnownAccount; 1249 | 1250 | let component = new List({ 1251 | type: t.list(Account), 1252 | ctx: ctx, 1253 | options: {}, 1254 | value: [{ type: "type 1" }] 1255 | }); 1256 | 1257 | assert.strictEqual(component.getItems()[0].input.props.type, KnownAccount); 1258 | 1259 | component = new List({ 1260 | type: t.list(Account), 1261 | ctx: ctx, 1262 | options: {}, 1263 | value: [{ type: "other" }] 1264 | }); 1265 | 1266 | assert.strictEqual(component.getItems()[0].input.props.type, UnknownAccount); 1267 | }); 1268 | --------------------------------------------------------------------------------