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