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