├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── GUIDE.md
├── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── docs
└── example.png
├── karma.conf.js
├── package.json
├── src
├── components.js
├── i18n
│ ├── de.js
│ ├── en.js
│ ├── es.js
│ ├── fr.js
│ ├── it.js
│ └── uk.js
├── index.js
├── main.js
└── util.js
├── test
├── components
│ ├── Checkbox.js
│ ├── Component.js
│ ├── Datetime.js
│ ├── List.js
│ ├── Radio.js
│ ├── Select.js
│ ├── Struct.js
│ ├── Textbox.js
│ ├── index.js
│ └── util.js
├── index.js
└── util.js
└── webpack.dist.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | stage: 0,
3 | loose: true
4 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "eslint-config-airbnb",
4 | "env": {
5 | "browser": true,
6 | "node": true
7 | },
8 | "plugins": [
9 | "react"
10 | ],
11 | "rules": {
12 | "semi": [2, "never"],
13 | "comma-dangle": 0,
14 | "react/sort-comp": 0,
15 | "react/no-multi-comp": 0,
16 | "react/prop-types": 0
17 | }
18 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | dev
3 | dist
4 | node_modules
5 | lib
6 | .idea
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "9"
4 | before_script:
5 | - export DISPLAY=:99.0
6 | - sh -e /etc/init.d/xvfb start
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | > **Tags:**
4 | > - [New Feature]
5 | > - [Bug Fix]
6 | > - [Breaking Change]
7 | > - [Documentation]
8 | > - [Internal]
9 | > - [Polish]
10 | > - [Experimental]
11 |
12 | **Note**: Gaps between patch versions are faulty/broken releases.
13 | **Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.
14 |
15 | ## 0.9.21
16 |
17 | - **Polish**
18 | - Possibility to use flexible sorting, PR #422 (@yasaricli)
19 |
20 | ## 0.9.20
21 |
22 | - **Bug Fix**
23 | - remove deleted refs keys from childRefs registry (@gbiryukov)
24 |
25 | ## 0.9.18
26 |
27 | - **Internal**
28 | - replace string refs with callback refs (@gbiryukov)
29 | - add React 16 to `peerDependencies` (@gcanti)
30 |
31 | ## 0.9.17
32 |
33 | - **Bug Fix**
34 | - Respect path changes in Component `shouldComponentUpdate`, fix #388 (@gbiryukov)
35 |
36 | ## 0.9.16
37 |
38 | - **New Feature**
39 | - German translation (@jpJuni0r)
40 |
41 | ## 0.9.15
42 |
43 | - **New Feature**
44 | - French translation (@jebarjonet)
45 |
46 | ## v0.9.14
47 |
48 | - **Bug Fix**
49 | - make `numberTransformer` resistant to minification (UglifyJS with `unsafe_comps` option) (@gbiryukov)
50 |
51 | ## v0.9.13
52 |
53 | - **Bug Fix**
54 | - revert last commit
55 |
56 | ## v0.9.12
57 |
58 | - **New Feature**
59 | - Add item parameter to List.addItem function (@karlguillotte)
60 |
61 | ## v0.9.11
62 |
63 | - **Experimental**
64 | - optional `getTcombFormOptions` function on types
65 |
66 | ## v0.9.10
67 |
68 | - **Bug Fix**
69 | - struct's and list's validate() now set hasError to true when there are errors, fix #350 (@gcanti, thanks @volkanunsal)
70 |
71 | ## v0.9.9
72 |
73 | - **Bug Fix**
74 | - retain static functions when constructing concrete type from union (@minedeljkovic)
75 |
76 | ## v0.9.8
77 |
78 | - **Bug Fix**
79 | - `_nativeContainerInfo` no longer exists in React v15.2.0, use `_hostContainerInfo` instead, fix #345 (thanks @kikoanis)
80 | - `transformer.parse` was not called for structs and lists (@gcanti)
81 | - **Experimental**
82 | - add support for interfaces (tcomb ^3.1.0), fix #341 (@gcanti)
83 |
84 | ## v0.9.7
85 |
86 | - **New Feature**
87 | - add support for union options (thanks @minedeljkovic)
88 |
89 | ## v0.9.6
90 |
91 | - **Bug Fix**
92 | - Broken with jspm, fix #331 (thanks @abhishiv)
93 |
94 | ## v0.9.5
95 |
96 | - **Bug Fix**
97 | - check for existing index in `List`'s `getValue` method, fix #322 (thanks @rajeshps)
98 |
99 | ## v0.9.4
100 |
101 | - **Bug Fix**
102 | - fix broken server side rendering with React v15.0.0, fix https://github.com/gcanti/tcomb-form-templates-bootstrap/issues/7
103 | - Optional or subtyped unions, fix #319
104 |
105 | ## v0.9.3
106 |
107 | - **Internal**
108 | - Move React to peerDependecies and devDependencies, fix #313 (thanks @maksis)
109 |
110 | ## v0.9.2
111 |
112 | - **Internal**
113 | - use empty string instead of null in textbox format, fix #308 (thanks @snadn). Reference https://facebook.github.io/react/blog/#new-deprecations-introduced-with-a-warning
114 |
115 | ## v0.9.1
116 |
117 | - **Bug Fix**
118 | - upgrade to `tcomb-form-templates-bootstrap` v0.2, ref https://github.com/gcanti/tcomb-json-schema/issues/22
119 |
120 | ## v0.9.0
121 |
122 | **Warning**. If you don't rely in your codebase on the property `maybe(MyType)(undefined) === null` this **is not a breaking change** for you.
123 |
124 | - **Breaking Change**
125 | - upgrade to `tcomb-validation` v3.0.0
126 | - **Polish**
127 | - remove `evt.preventDefault()` calls
128 |
129 | ## v0.8.2
130 |
131 | - **New Feature**
132 | - now `options` can also be a function `(value: any) -> object`
133 | - support for unions, fix #297
134 | - add new `isPristine` field to components `state`
135 | - **Documentation**
136 | - add issue template (new GitHub feature)
137 |
138 | ## v0.8.1
139 |
140 | - **New Feature**
141 | - add dist configuration for [unpkg](https://unpkg.com/)
142 |
143 | ## v0.8.0
144 |
145 | - **Breaking Change**
146 | - drop `uvdom`, `uvdom-bootstrap` dependencies
147 | - bootstrap templates in its own repo [tcomb-form-templates-bootstrap](https://github.com/gcanti/tcomb-form-templates-bootstrap)
148 | - semantic templates in its own repo [tcomb-form-templates-semantic](https://github.com/gcanti/tcomb-form-templates-semantic)
149 |
150 | **Migration guide**
151 |
152 | `tcomb-form` follows semver and technically this is a breaking change (hence the minor version bump).
153 | However, if you are using the default bootstrap templates, the default language (english) and you are not relying on the `uvdom` and `uvdom-bootstrap` modules, this is **not a breaking change** for you.
154 |
155 | ### How to
156 |
157 | **I'm using the default bootstrap templates and the default language (english)**
158 |
159 | This is easy: nothing changed for you.
160 |
161 | **I'm using the default bootstrap templates but I override the language**
162 |
163 | ```diff
164 | var t = require('tcomb-form/lib');
165 | -var templates = require('tcomb-form/lib/templates/bootstrap');
166 | +var templates = require('tcomb-form/node_modules/tcomb-form-templates-bootstrap');
167 |
168 | t.form.Form.templates = templates;
169 | t.form.Form.i18n = {
170 | ...
171 | };
172 | ```
173 |
174 | (contributions to `src/i18n` folder welcome!)
175 |
176 | **I'm using the default language (english) but I override the templates**
177 |
178 | ```sh
179 | npm install tcomb-form-templates-semantic --save
180 | ```
181 |
182 | ```diff
183 | var t = require('tcomb-form/lib');
184 | var i18n = require('tcomb-form/lib/i18n/en');
185 | -var templates = require('tcomb-form/lib/templates/semantic');
186 | +var templates = require('tcomb-form-templates-semantic');
187 |
188 | t.form.Form.i18n = i18n;
189 | t.form.Form.templates = templates;
190 | ```
191 |
192 | ## v0.7.10
193 |
194 | - **Bug Fix**
195 | - IE8 issue, 'this.refs.input' is null or not and object, fix #268
196 |
197 | ## v0.7.9
198 |
199 | - **Bug Fix**
200 | - use keys returned from getTypeProps as refs, #269
201 |
202 | ## v0.7.8
203 |
204 | **Warning**. `uvdom` dependency is deprecated and will be removed in the next releases. If you are using custom templates based on `uvdom`, please add a static function `toReactElement` before upgrading to v0.8:
205 |
206 | ```js
207 | const Type = t.struct({
208 | name: t.String
209 | })
210 |
211 | import { compile } from 'uvdom/react'
212 |
213 | function myTemplate(locals) {
214 | return {tag: 'input', attrs: { value: locals.value }}
215 | }
216 |
217 | myTemplate.toReactElement = compile // <= here
218 |
219 | const options = {
220 | fields: {
221 | name: {
222 | template: myTemplate
223 | }
224 | }
225 | }
226 | ```
227 |
228 | - **New Feature**
229 | - complete refactoring of bootstrap templates, fix #254
230 | - add a type property to button locals
231 | - one file for each template
232 | - every template own a series of render* function that can be overridden
233 |
234 | **Example**
235 |
236 | ```js
237 | const Type = t.struct({
238 | name: t.String
239 | })
240 |
241 | const myTemplate = t.form.Form.templates.textbox.clone({
242 | // override default implementation
243 | renderInput: (locals) => {
244 | return
245 | }
246 | })
247 |
248 | const options = {
249 | fields: {
250 | name: {
251 | template: myTemplate
252 | }
253 | }
254 | }
255 | ```
256 |
257 | - more style classes for styling purposes, fix #171
258 |
259 | **Example**
260 |
261 | ```js
262 | const Type = t.struct({
263 | name: t.String,
264 | rememberMe: t.Boolean
265 | })
266 | ```
267 |
268 | outputs
269 |
270 | ```html
271 |
272 |
281 | ```
282 |
283 | - complete refactoring of semantic templates
284 | - add a type property to button locals
285 | - one file for each template
286 | - every template own a series of render* function that can be overridden
287 | - more style classes for styling purposes, fix #171
288 | - add `context` prop to template `locals`
289 | - **Bug Fix**
290 | - Incosistent calling of tcomb-validation `validate` function in `getTypeInfo` and components for struct and list types, fix #253
291 | - avoid useless re-renderings of Datetime when the value is undefined
292 | - **Experimental**
293 | - if a type owns a `getTcombFormFactory(options)` static function, it will be used to retrieve the suitable factory
294 |
295 | **Example**
296 |
297 | ```js
298 | // instead of
299 | const Country = t.enums.of(['IT', 'US'], 'Country');
300 |
301 | const Type = t.struct({
302 | country: Country
303 | });
304 |
305 | const options = {
306 | fields: {
307 | country: {
308 | factory: t.form.Radio
309 | }
310 | }
311 | };
312 |
313 | // you can write
314 | const Country = t.enums.of(['IT', 'US'], 'Country');
315 |
316 | Country.getTcombFormFactory = function (/*options*/) {
317 | return t.form.Radio;
318 | };
319 |
320 | const Type = t.struct({
321 | country: Country
322 | });
323 |
324 | const options = {};
325 | ```
326 |
327 | - **Internal**
328 | - remove `raw` param in `getValue` API (use `validate()` API instead)
329 | - remove deprecated types short alias from tests
330 | - factor out UIDGenerator from `Form` render method
331 | - optimize `getError()` return an error message only if `hasError === true`
332 |
333 | ## v0.7.6
334 |
335 | - **Bug Fix**
336 | - de-optimise structs / lists onChange, fix #235
337 | - **Experimental**
338 | - add support for maybe structs and maybe lists, fix #236
339 |
340 | ## v0.7.5
341 |
342 | - **Bug Fix**
343 | - optional refinement with custom error message not passing locals.error, fix #230
344 | - Kind is undefined in onChange for nested List, fix #231
345 | - **Internal**
346 | - custom error function now takes a parsed value
347 |
348 | ## v0.7.4
349 |
350 | - **New Feature**
351 | - pass the component options to the error option function, fix #222
352 | - **Bug Fix**
353 | - Inconsistent error message creation process between `validate(val, type)` and form validation, fix #221
354 | - Radio component does not have a transformer in IE10-, fix #226
355 | - **Internal**
356 | - upgrade to react v0.14
357 |
358 | ## v0.7.3
359 |
360 | - **New Feature**
361 | - add the type to template locals, fix #210
362 | - **Bug Fix**
363 | - Fields are wrapped in a form-group, fix #215
364 |
365 | ## v0.7.2
366 |
367 | - **Bug Fix**
368 | - Add className to locals of Struct and List (thanks @jsor)
369 |
370 | ## v0.7.1
371 |
372 | - **Internal**
373 | - upgrade to latest version of tcomb-validation (2.2.0)
374 | - removed react-dom dependency
375 | - removed debug dependency
376 | - **New Feature**
377 | - added argument `context` to `error` options that are functions (new signature: `error(value, path, context)`)
378 | - added `error` option default if the type constructor owns a `getValidationErrorMessage(value, path, context)` function
379 | - added `context` prop to all components (passed into `error` as `context` argument)
380 |
381 | ## v0.7.0
382 |
383 | - **Breaking Change**
384 | - upgrade to react v0.14.0-rc1, fix #194
385 |
386 | ## v0.6.4
387 |
388 | - **New Feature**
389 | - added a `required` field to i18n, fix #181
390 |
391 | ## v0.6.3
392 |
393 | - **Bug Fix**
394 | - `help` option for `t.Dat` gives bootstrap error #164
395 |
396 | ## v0.6.2
397 |
398 | - **Internal**
399 | + upgrade to latest version of tcomb-validation (2.0.0)
400 |
401 | ## v0.6.1
402 |
403 | - **Internal**
404 | + memoise `t.form.Component::getId()`
405 | - **Bug Fix**
406 | + Encountered two children with the same key fix #152
407 |
408 | ## v0.6.0
409 |
410 | - **Breaking Change**
411 | + upgrade to tcomb-validation v2.0.0-beta
412 |
413 | ## v0.5.5
414 |
415 | - **Internal**
416 | + Relax Bootstrap columns constraint #149
417 |
418 | ## v0.5.4
419 |
420 | - **Bug Fix**
421 | + Unable to disable t.Dat field #137
422 | - **New Feature**
423 | + t.Dat basic template (semantic skin)
424 |
425 | ## v0.5.3
426 |
427 | - **New Feature**
428 | + Add buttonBefore support (bootstrap skin) #126
429 | - **Bug Fix**
430 | + fix server-side rendering markup differences #124
431 | - **Internal**
432 | + tcomb-validation: upgrade to latest version
433 |
434 | ## v0.5.2
435 |
436 | - **New Feature**
437 | + Add buttonAfter support (bootstrap skin) #126
438 | - **Documentation**
439 | + Add `attrs` option
440 | + Add Date input
441 | + Fix wrong imports in Customizations section
442 | - **Internal**
443 | + Optimise stuct and list re-rendering on value change
444 | + fix server-side rendering markup differences #124
445 |
446 | ## v0.5.1
447 |
448 | - **Bug Fix**
449 | + Remove wrong label id attribute in date template (bootstrap skin)
450 |
451 | ## v0.5
452 |
453 | - **New Feature**
454 | + Add attributes and events (`attrs` option) #76, #91, #53, #67
455 | + Add bootstrap static control, #92
456 | + Set class on form-group div indicating its depth within form, #64
457 | + Added `kind` param to `onChange` handler
458 | - **Breaking Change**
459 | + Drop support for React v0.12.x
460 | + Moved `placeholder` option within `attrs` option
461 | + Moved `id` option within `attrs` option
462 | - **Bug Fix**
463 | + Remove class "has-error" from empty optional numeric field #113
464 | - **Documentation**
465 | + Add GUIDE.md
466 | - **Internal**
467 | + Complete code refactoring
468 |
469 | ## v0.4.10
470 |
471 | - **New Feature**
472 | + Add experimental semantic ui skin
473 | - **Bug Fix**
474 | + Fix `nullOption` was incorrectly added to multiple selects
475 |
476 | ## v0.4.9
477 |
478 | - **New Feature**
479 | + added `template` option to structs and lists
480 | + added `Component.extend` API
481 | + added `getComponent(path)` API
482 | + added basic date component
483 | - **Internal**
484 | + complete code refactoring
485 |
--------------------------------------------------------------------------------
/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 Guidelines
7 |
8 | Before you submit an issue, check that it meets these guidelines:
9 |
10 | - specify the version of `tcomb-form` you are using
11 | - specify the version of `react` you are using
12 | - if the issue regards a bug, please provide a minimal failing example / test (see "How to setup an example on codepen.io")
13 |
14 | ## How to setup an example on [codepen.io](http://codepen.io/)
15 |
16 | Configuration:
17 |
18 | **CSS**
19 |
20 | - https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css
21 |
22 | **JS**
23 |
24 | JavaScript Preprocessor: Babel
25 |
26 | - //cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js
27 | - //cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js
28 | - https://unpkg.com/tcomb-form/dist/tcomb-form.js (or https://unpkg.com/tcomb-form/dist/tcomb-form.min.js)
29 |
30 | ## Pull Request Guidelines
31 |
32 | Before you submit a pull request from your forked repo, check that it meets these guidelines:
33 |
34 | 1. If the pull request fixes a bug, it should include tests that fail without the changes, and pass
35 | with them.
36 | 2. If the pull request adds functionality, the docs should be updated as part of the same PR.
37 | 3. Please rebase and resolve all conflicts before submitting.
38 |
39 | ## Setting up your environment
40 |
41 | After forking tcomb-form to your own github org, do the following steps to get started:
42 |
43 | ```sh
44 | # clone your fork to your local machine
45 | git clone https://github.com/gcanti/tcomb-form.git
46 |
47 | # step into local repo
48 | cd tcomb-form
49 |
50 | # install dependencies
51 | npm install
52 |
53 | # build lib folder (optional)
54 | npm run watch
55 | ```
56 |
57 | ### Running Tests
58 |
59 | ```sh
60 | # run tests
61 | npm test
62 | ```
63 |
64 | ### Style & Linting
65 |
66 | This codebase adheres to the [Airbnb Styleguide](https://github.com/airbnb/javascript) with a few tweaks and is
67 | enforced using [ESLint](http://eslint.org/).
68 |
69 | It is recommended that you install an eslint plugin for your editor of choice when working on this
70 | codebase, however you can always check to see if the source code is compliant by running:
71 |
72 | ```sh
73 | npm run lint
74 | ```
75 |
76 |
--------------------------------------------------------------------------------
/GUIDE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | **Table of Contents**
4 |
5 | - [Get started](#get-started)
6 | - [Setup](#setup)
7 | - [Working example](#working-example)
8 | - [API](#api)
9 | - [`getValue()`](#getvalue)
10 | - [`validate()`](#validate)
11 | - [How to](#how-to)
12 | - [Adding a default value and listening to changes](#adding-a-default-value-and-listening-to-changes)
13 | - [Reset the form](#reset-the-form)
14 | - [Accessing fields](#accessing-fields)
15 | - [Submitting the form](#submitting-the-form)
16 | - [Customised error messages](#customised-error-messages)
17 | - [List with Dynamic Items (Different structs based on selected value)](#list-with-dynamic-items-different-structs-based-on-selected-value)
18 | - [Types](#types)
19 | - [Required field](#required-field)
20 | - [Optional field](#optional-field)
21 | - [Numbers](#numbers)
22 | - [Refinements](#refinements)
23 | - [Booleans](#booleans)
24 | - [Dates](#dates)
25 | - [Enums](#enums)
26 | - [Lists](#lists)
27 | - [Nested structures](#nested-structures)
28 | - [Rendering options](#rendering-options)
29 | - [Struct options](#struct-options)
30 | - [Automatically generated placeholders](#automatically-generated-placeholders)
31 | - [Fields order](#fields-order)
32 | - [Legend](#legend)
33 | - [Help message](#help-message)
34 | - [Error messages](#error-messages)
35 | - [Disabled](#disabled)
36 | - [Fields configuration](#fields-configuration)
37 | - [Look and feel](#look-and-feel)
38 | - [List options](#list-options)
39 | - [Items configuration](#items-configuration)
40 | - [Internationalization](#internationalization)
41 | - [Buttons configuration](#buttons-configuration)
42 | - [Union options](#union-options)
43 | - [Textbox options](#textbox-options)
44 | - [Type attribute](#type-attribute)
45 | - [Label](#label)
46 | - [Attributes and events](#attributes-and-events)
47 | - [Styling](#styling)
48 | - [Checkbox options](#checkbox-options)
49 | - [Select options](#select-options)
50 | - [Null option](#null-option)
51 | - [Options order](#options-order)
52 | - [Custom options](#custom-options)
53 | - [Render as a radio group](#render-as-a-radio-group)
54 | - [Multiple select](#multiple-select)
55 | - [Date options](#date-options)
56 | - [Fields order](#fields-order-1)
57 | - [Customizations](#customizations)
58 | - [Templates](#templates)
59 | - [Clone default templates](#clone-default-templates)
60 | - [Transformers](#transformers)
61 | - [Custom factories](#custom-factories)
62 | - [getTcombFormFactory](#gettcombformfactory)
63 | - [getTcombFormOptions](#gettcombformoptions)
64 | - [Bootstrap extras](#bootstrap-extras)
65 | - [Textbox](#textbox)
66 | - [Addons](#addons)
67 | - [Size](#size)
68 | - [Select](#select)
69 | - [Struct](#struct)
70 | - [General configuration](#general-configuration)
71 | - [Changing the default language](#changing-the-default-language)
72 | - [Changing the default skin](#changing-the-default-skin)
73 |
74 |
75 |
76 | # Get started
77 |
78 | ## Setup
79 |
80 | ```sh
81 | $ npm install tcomb-form
82 | ```
83 | Note: Use tcomb-form@0.6.x with react@0.13.x. See [#200](https://github.com/gcanti/tcomb-form/issues/200).
84 |
85 | ## Working example
86 |
87 | ```js
88 | import React from 'react';
89 | import { render } from 'react-dom';
90 | import t from 'tcomb-form';
91 |
92 | const Form = t.form.Form;
93 |
94 | // define your domain model with tcomb
95 | // https://github.com/gcanti/tcomb
96 | const Person = t.struct({
97 | name: t.String,
98 | surname: t.String
99 | });
100 |
101 | const App = React.createClass({
102 |
103 | save() {
104 | // call getValue() to get the values of the form
105 | var value = this.refs.form.getValue();
106 | // if validation fails, value will be null
107 | if (value) {
108 | // value here is an instance of Person
109 | console.log(value);
110 | }
111 | },
112 |
113 | render() {
114 | return (
115 |
116 |
120 |
121 |
122 | );
123 | }
124 |
125 | });
126 |
127 | render(, document.getElementById('app'));
128 | ```
129 |
130 | > **Note**. Labels are automatically generated.
131 |
132 | ## API
133 |
134 | ### `getValue()`
135 |
136 | Returns `null` if the validation failed; otherwise returns an instance of your model.
137 |
138 | > **Note**. Calling `getValue` will cause the validation of all the fields of the form, including some side effects like highlighting the errors.
139 |
140 | ### `validate()`
141 |
142 | Returns a `ValidationResult` (see [tcomb-validation](https://github.com/gcanti/tcomb-validation) for a reference documentation).
143 |
144 | ## How to
145 |
146 | ### Adding a default value and listening to changes
147 |
148 | The `Form` component behaves like a [controlled component](https://facebook.github.io/react/docs/forms.html):
149 |
150 | ```js
151 | const App = React.createClass({
152 |
153 | getInitialState() {
154 | return {
155 | value: {
156 | name: 'Giulio',
157 | surname: 'Canti'
158 | }
159 | };
160 | },
161 |
162 | onChange(value) {
163 | this.setState({value});
164 | },
165 |
166 | save() {
167 | var value = this.refs.form.getValue();
168 | if (value) {
169 | console.log(value);
170 | }
171 | },
172 |
173 | render() {
174 | return (
175 |
176 |
182 |
183 |
184 | );
185 | }
186 |
187 | });
188 | ```
189 |
190 | The `onChange` handler has the following signature:
191 |
192 | ```
193 | (raw: any, path: Array, kind?) => void
194 | ```
195 |
196 | where
197 |
198 | - `raw` contains the current raw value of the form (can be an invalid value for your model)
199 | - `path` is the path to the field triggering the change
200 | - `kind` specify the kind of change (when `undefined` means a value change)
201 | + `'add'` list item added
202 | + `'remove'` list item removed
203 | + `'moveUp'` list item moved up
204 | + `'moveDown'` list item moved down
205 |
206 | ### Reset the form
207 |
208 | In the previous example, just pass `null` to the `value` prop:
209 |
210 | ```js
211 | const App = React.createClass({
212 |
213 | ...
214 |
215 | resetForm() {
216 | this.setState({value: null});
217 | },
218 |
219 | save() {
220 | var value = this.refs.form.getValue();
221 | if (value) {
222 | console.log(value);
223 | // clear all fields after submit
224 | this.resetForm();
225 | }
226 | },
227 |
228 | ...
229 |
230 | });
231 | ```
232 |
233 | ### Accessing fields
234 |
235 | You can get access to a field with the `getComponent(path)` API:
236 |
237 | ```js
238 | const Person = t.struct({
239 | name: t.String,
240 | surname: t.String
241 | });
242 |
243 | const App = React.createClass({
244 |
245 | onChange(value, path) {
246 | // validate a field on every change
247 | this.refs.form.getComponent(path).validate();
248 | },
249 |
250 | render() {
251 | return (
252 |
253 |
257 |
258 | );
259 | }
260 |
261 | });
262 | ```
263 |
264 | ### Submitting the form
265 |
266 | The output of the `Form` component is a `fieldset` tag containing your fields. You can submit the form by wrapping the output with a `form` tag:
267 |
268 | ```js
269 | const App = React.createClass({
270 |
271 | onSubmit(evt) {
272 | const value = this.refs.form.getValue();
273 | if (!value) {
274 | // there are errors, don't send the form
275 | evt.preventDefault();
276 | } else {
277 | // everything ok, let the form fly...
278 | // ...or handle the values contained in the
279 | // `value` variable with js
280 | }
281 | },
282 |
283 | render() {
284 | return (
285 |
290 |
291 |
292 | );
293 | }
294 |
295 | });
296 | ```
297 |
298 | ### Customised error messages
299 |
300 | See [Error messages](#error-messages) section.
301 |
302 | ### List with Dynamic Items (Different structs based on selected value)
303 |
304 | 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:
305 |
306 | ```js
307 | const AccountType = t.enums.of([
308 | 'type 1',
309 | 'type 2',
310 | 'other'
311 | ], 'AccountType')
312 |
313 | const KnownAccount = t.struct({
314 | type: AccountType
315 | }, 'KnownAccount')
316 |
317 | // UnknownAccount extends KnownAccount so it owns also the type field
318 | const UnknownAccount = KnownAccount.extend({
319 | label: t.String,
320 | }, 'UnknownAccount')
321 |
322 | // the union
323 | const Account = t.union([KnownAccount, UnknownAccount], 'Account')
324 |
325 | // the final form type
326 | const Type = t.list(Account)
327 | ```
328 |
329 | 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:
330 |
331 | ```js
332 | // if account type is 'other' return the UnknownAccount type
333 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount
334 | ```
335 |
336 | The complete example:
337 |
338 | ```js
339 | import React from 'react'
340 | import t from 'tcomb-form'
341 |
342 | const AccountType = t.enums.of([
343 | 'type 1',
344 | 'type 2',
345 | 'other'
346 | ], 'AccountType')
347 |
348 | const KnownAccount = t.struct({
349 | type: AccountType
350 | }, 'KnownAccount')
351 |
352 | const UnknownAccount = KnownAccount.extend({
353 | label: t.String,
354 | }, 'UnknownAccount')
355 |
356 | const Account = t.union([KnownAccount, UnknownAccount], 'Account')
357 |
358 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount
359 |
360 | const Type = t.list(Account)
361 |
362 | const App = React.createClass({
363 |
364 | onSubmit(evt) {
365 | evt.preventDefault()
366 | const v = this.refs.form.getValue()
367 | if (v) {
368 | console.log(v)
369 | }
370 | },
371 |
372 | render() {
373 | return (
374 |
383 | )
384 | }
385 |
386 | })
387 | ```
388 |
389 | # Types
390 |
391 | Models are defined with [tcomb](https://github.com/gcanti/tcomb). tcomb is a library for Node.js and the browser which allows you to check the types of JavaScript values at runtime with a simple syntax. It's great for Domain Driven Design, for testing, and for adding safety to your internal code.
392 |
393 | ## Required field
394 |
395 | By default fields are required:
396 |
397 | ```js
398 | const Person = t.struct({
399 | name: t.String, // a required string
400 | surname: t.String // a required string
401 | });
402 | ```
403 |
404 | ## Optional field
405 |
406 | In order to create an optional field, wrap the field type with the `t.maybe` combinator:
407 |
408 | ```js
409 | const Person = t.struct({
410 | name: t.String,
411 | surname: t.String,
412 | email: t.maybe(t.String) // an optional string
413 | });
414 | ```
415 |
416 | The suffix `" (optional)"` is automatically added to optional fields.
417 |
418 | You can customise the suffix value, or set a suffix for required fields (see the "Internationalization" section).
419 |
420 | ## Numbers
421 |
422 | In order to create a numeric field, use the `t.Number` type:
423 |
424 | ```js
425 | const Person = t.struct({
426 | name: t.String,
427 | surname: t.String,
428 | email: t.maybe(t.String),
429 | age: t.Number // a numeric field
430 | });
431 | ```
432 |
433 | tcomb-form will automatically convert numbers to / from strings.
434 |
435 | ## Refinements
436 |
437 | A *predicate* is a function with the following signature:
438 |
439 | ```
440 | (x: any) => boolean
441 | ```
442 |
443 | You can refine a type with the `t.refinement(type, predicate)` combinator:
444 |
445 | ```js
446 | // a type representing positive numbers
447 | const Positive = t.refinement(t.Number, (n) => n >= 0});
448 |
449 | const Person = t.struct({
450 | name: t.String,
451 | surname: t.String,
452 | email: t.maybe(t.String),
453 | age: Positive
454 | });
455 | ```
456 |
457 | Refinements allow you to express any custom validation with a simple predicate.
458 |
459 | ## Booleans
460 |
461 | In order to create a boolean field, use the `t.Boolean` type:
462 |
463 | ```js
464 | const Person = t.struct({
465 | name: t.String,
466 | surname: t.String,
467 | email: t.maybe(t.String),
468 | age: t.Number,
469 | rememberMe: t.Boolean // a boolean field
470 | });
471 | ```
472 |
473 | Booleans are displayed as checkboxes.
474 |
475 | ## Dates
476 |
477 | In order to create a date field, use the `t.Date` type:
478 |
479 | ```js
480 | const Person = t.struct({
481 | name: t.String,
482 | surname: t.String,
483 | email: t.maybe(t.String),
484 | age: t.Number,
485 | rememberMe: t.Boolean,
486 | birthDate: t.Date // a date field
487 | });
488 | ```
489 |
490 | ## Enums
491 |
492 | In order to create an enum field, use the `t.enums` combinator:
493 |
494 | ```js
495 | const Gender = t.enums({
496 | M: 'Male',
497 | F: 'Female'
498 | });
499 |
500 | const Person = t.struct({
501 | name: t.String,
502 | surname: t.String,
503 | email: t.maybe(t.String),
504 | age: t.Number,
505 | rememberMe: t.Boolean,
506 | birthDate: t.Date,
507 | gender: Gender // enum
508 | });
509 | ```
510 |
511 | By default enums are displayed as selects.
512 |
513 | ## Lists
514 |
515 | You can handle a list with the `t.list` combinator:
516 |
517 | ```js
518 | const Person = t.struct({
519 | name: t.String,
520 | surname: t.String,
521 | email: t.maybe(t.String),
522 | age: Positive, // refinement
523 | rememberMe: t.Boolean,
524 | birthDate: t.Date,
525 | gender: Gender,
526 | tags: t.list(t.String) // a list of strings
527 | });
528 | ```
529 |
530 | ## Nested structures
531 |
532 | You can nest lists and structs at an arbitrary level:
533 |
534 | ```js
535 | const Person = t.struct({
536 | name: t.String,
537 | surname: t.String
538 | });
539 |
540 | const Persons = t.list(Person);
541 |
542 | ...
543 |
544 |
548 | ```
549 |
550 | # Rendering options
551 |
552 | In order to customise the look and feel, use an `options` prop:
553 |
554 | ```js
555 |
556 | ```
557 |
558 | > **Warning**. tcomb-form uses shouldComponentUpdate aggressively. In order to ensure that tcomb-form detect any change to `type`, `options` or `value` props you have to change references:
559 |
560 | Example: disable a field based on another field's value
561 |
562 | ```js
563 | const Type = t.struct({
564 | disable: t.Boolean, // if true, name field will be disabled
565 | name: t.String
566 | });
567 |
568 | const options = {
569 | fields: {
570 | name: {}
571 | }
572 | };
573 |
574 | const App = React.createClass({
575 |
576 | getInitialState() {
577 | return {
578 | options: options,
579 | value: null
580 | };
581 | },
582 |
583 | onSubmit(evt) {
584 | evt.preventDefault();
585 | var value = this.refs.form.getValue();
586 | if (value) {
587 | console.log(value);
588 | }
589 | },
590 |
591 | onChange(value) {
592 | // tcomb immutability helpers
593 | // https://github.com/gcanti/tcomb/blob/master/GUIDE.md#updating-immutable-instances
594 | var options = t.update(this.state.options, {
595 | fields: {
596 | name: {
597 | disabled: {'$set': value.disable}
598 | }
599 | }
600 | });
601 | this.setState({options: options, value: value});
602 | },
603 |
604 | render() {
605 | return (
606 |
613 |
614 |
615 | );
616 | }
617 |
618 | });
619 | ```
620 |
621 | ## Struct options
622 |
623 | ### Automatically generated placeholders
624 |
625 | In order to generate default placeholders use the option `auto: 'placeholders'`:
626 |
627 | ```js
628 | const options = {
629 | auto: 'placeholders'
630 | };
631 |
632 |
633 | ```
634 |
635 | Or `auto: 'none'` if you don't want neither labels nor placeholders:
636 |
637 | ```js
638 | const options = {
639 | auto: 'none'
640 | };
641 | ```
642 |
643 | ### Fields order
644 |
645 | You can sort the fields with the `order` option:
646 |
647 | ```js
648 | const options = {
649 | order: ['name', 'surname', 'rememberMe', 'gender', 'age', 'email']
650 | };
651 | ```
652 |
653 | > **Warning**: Any field that is not in this array will be omitted from the form
654 |
655 | ### Legend
656 |
657 | You can add a fieldset legend with the `legend` option:
658 |
659 | ```js
660 | const options = {
661 | // you can use strings or JSX
662 | legend: My form legend
663 | };
664 | ```
665 |
666 | ### Help message
667 |
668 | You can add an help message with the `help` option:
669 |
670 | ```js
671 | const options = {
672 | // you can use strings or JSX
673 | help: My form help
674 | };
675 | ```
676 |
677 | ### Error messages
678 |
679 | You can add a custom error message with the `error` option:
680 |
681 | ```js
682 | const options = {
683 | error: A custom error message // use strings or JSX
684 | };
685 | ```
686 |
687 | `error` can also be a function with the following signature:
688 |
689 | ```
690 | type getValidationErrorMessage = (value, path, context) => ?(string | ReactElement)
691 | ```
692 |
693 | where
694 |
695 | - `value` is the (parsed) current value of the component.
696 | - `path` is the path of the value being validated
697 | - `context` is the value of the `context` prop. Also it contains a reference to the component options.
698 |
699 | The value returned by the function will be used as error message.
700 |
701 | If you want to show the error message onload, add the `hasError` option:
702 |
703 | ```js
704 | const options = {
705 | hasError: true,
706 | error: A custom error message
707 | };
708 | ```
709 |
710 | Another (advanced) way to customize the error message is to add a:
711 |
712 | ```
713 | getValidationErrorMessage(value, path, context) => ?(string | ReactElement)
714 | ```
715 |
716 | static function to the type, where the arguments are the same as above:
717 |
718 | ```js
719 | const Age = t.refinement(t.Number, (n) => return n >= 18);
720 |
721 | // if you define a getValidationErrorMessage function, it will be called on validation errors
722 | Age.getValidationErrorMessage = (value, path, context) => {
723 | return 'bad age, locale: ' + context.locale;
724 | };
725 |
726 | const Schema = t.struct({
727 | age: Age
728 | });
729 |
730 | const App = React.createClass({
731 |
732 | onSubmit(evt) {
733 | evt.preventDefault();
734 | const value = this.refs.form.getValue();
735 | if (value) {
736 | console.log(value);
737 | }
738 | },
739 |
740 | render() {
741 | return (
742 |
750 | );
751 | }
752 |
753 | });
754 | ```
755 |
756 | You can even define `getValidationErrorMessage` on the supertype in order to be DRY:
757 |
758 | ```js
759 | t.Number.getValidationErrorMessage = (value, path, context) => {
760 | return 'bad number';
761 | };
762 |
763 | Age.getValidationErrorMessage = (value, path, context) => {
764 | return 'bad age, locale: ' + context.locale;
765 | };
766 | ```
767 |
768 | ### Disabled
769 |
770 | You can disable the whole fieldset with the `disabled` option:
771 |
772 | ```js
773 | const options = {
774 | disabled: true
775 | };
776 | ```
777 |
778 | ### Fields configuration
779 |
780 | You can configure each field with the `fields` option:
781 |
782 | ```js
783 | const options = {
784 | fields: {
785 | name: {
786 | // name field configuration here..
787 | },
788 | surname: {
789 | // surname field configuration here..
790 | },
791 | ...
792 | }
793 | });
794 | ```
795 |
796 | `fields` is a hash containing a key for each field you want to configure.
797 |
798 | ### Look and feel
799 |
800 | You can customise the look and feel with the `template` option:
801 |
802 | ```js
803 | const options = {
804 | template: mytemplate // see Templates section for documentation
805 | }
806 | ```
807 |
808 | ## List options
809 |
810 | The following options are similar to the Struct ones:
811 |
812 | - `auto`
813 | - `disabled`
814 | - `help`
815 | - `hasError`
816 | - `error`
817 | - `legend`
818 | - `template`
819 |
820 | ### Items configuration
821 |
822 | To configure all the items in a list, set the `item` option:
823 |
824 | ```js
825 | const Colors = t.list(t.String);
826 |
827 | const options = {
828 | item: {
829 | type: 'color' // HTML5 type attribute
830 | }
831 | });
832 |
833 |
834 | ```
835 |
836 | ### Internationalization
837 |
838 | You can override the default language (english) with the `i18n` option:
839 |
840 | ```js
841 | const options = {
842 | i18n: {
843 | add: 'Nuovo', // add button
844 | down: 'Giù', // move down button
845 | optional: ' (opzionale)', // suffix added to optional fields
846 | required: '', // suffix added to required fields
847 | remove: 'Elimina', // remove button
848 | up: 'Su' // move up button
849 | }
850 | };
851 | ```
852 |
853 | ### Buttons configuration
854 |
855 | You can prevent operations on lists with the following options:
856 |
857 | - `disableAdd`: (default `false`) prevents adding new items
858 | - `disableRemove`: (default `false`) prevents removing existing items
859 | - `disableOrder`: (default `false`) prevents sorting existing items
860 |
861 | ```js
862 | const options = {
863 | disableOrder: true
864 | };
865 | ```
866 |
867 | ## Union options
868 |
869 | ```js
870 | const AccountType = t.enums.of([
871 | 'type 1',
872 | 'type 2',
873 | 'other'
874 | ], 'AccountType')
875 |
876 | const KnownAccount = t.struct({
877 | type: AccountType
878 | }, 'KnownAccount')
879 |
880 | // UnknownAccount extends KnownAccount so it owns also the type field
881 | const UnknownAccount = KnownAccount.extend({
882 | label: t.String,
883 | }, 'UnknownAccount')
884 |
885 | // the union
886 | const Account = t.union([KnownAccount, UnknownAccount], 'Account')
887 |
888 | // the final form type
889 | const Type = t.list(Account)
890 |
891 | const options = {
892 | item: [ // one options object for each concrete type of the union
893 | {
894 | label: 'KnownAccount'
895 | },
896 | {
897 | label: 'UnknownAccount'
898 | }
899 | ]
900 | }
901 | ```
902 |
903 | ## Textbox options
904 |
905 | > **Tech note**. Values containing only white spaces are converted to null.
906 |
907 | The following options are similar to the fieldset ones:
908 |
909 | - `disabled`
910 | - `help`
911 | - `hasError`
912 | - `error`
913 | - `template`
914 |
915 | ### Type attribute
916 |
917 | You can set the type attribute with the `type` option. The following values are allowed:
918 |
919 | - 'text' (default)
920 | - 'password'
921 | - 'hidden'
922 | - 'textarea' (outputs a textarea instead of a textbox)
923 | - 'static' (outputs a static value)
924 | - all the HTML5 type values
925 |
926 | ### Label
927 |
928 | You can override the default label with the `label` option:
929 |
930 | ```js
931 | const options = {
932 | fields: {
933 | name: {
934 | // you can use strings or JSX
935 | label: My label
936 | }
937 | }
938 | };
939 | ```
940 |
941 | ### Attributes and events
942 |
943 | You can add attributes and events with the `attrs` option:
944 |
945 | ```js
946 | const options = {
947 | fields: {
948 | name: {
949 | attrs: {
950 | autoFocus: true,
951 | placeholder: 'Type your name here',
952 | onBlur: () => {
953 | console.log('onBlur');
954 | }
955 | }
956 | }
957 | }
958 | };
959 | ```
960 |
961 |
962 | ### Styling
963 |
964 | You can a style class with the `className` or the `style` attribute:
965 |
966 | ```js
967 | const options = {
968 | fields: {
969 | name: {
970 | attrs: {
971 | className: 'myClassName'
972 | }
973 | }
974 | }
975 | };
976 | ```
977 |
978 | `className` can be a string, an array of strings or a dictionary `string -> boolean`.
979 |
980 | ## Checkbox options
981 |
982 | The following options are similar to the textbox ones:
983 |
984 | - `attrs`
985 | - `label`
986 | - `help`
987 | - `disabled`
988 | - `hasError`
989 | - `error`
990 | - `template`
991 |
992 | ## Select options
993 |
994 | The following options are similar to the textbox ones:
995 |
996 | - `attrs`
997 | - `label`
998 | - `help`
999 | - `disabled`
1000 | - `hasError`
1001 | - `error`
1002 | - `template`
1003 |
1004 | ### Null option
1005 |
1006 | You can customise the null option with the `nullOption` option:
1007 |
1008 | ```js
1009 | const options = {
1010 | fields: {
1011 | gender: {
1012 | nullOption: {value: '', text: 'Choose your gender'}
1013 | }
1014 | }
1015 | };
1016 | ```
1017 |
1018 | You can remove the null option by setting the `nullOption` option to `false`.
1019 |
1020 | > **Warning**: when you set `nullOption = false` you must also set the Form's `value` prop for the select field.
1021 |
1022 | > **Tech note**. A value equal to `nullOption.value` (default `''`) is converted to `null`.
1023 |
1024 | ### Options order
1025 |
1026 | You can sort the options with the `order` option:
1027 |
1028 | ```js
1029 | const options = {
1030 | fields: {
1031 | gender: {
1032 | order: 'asc' // or 'desc'
1033 | }
1034 | }
1035 | };
1036 | ```
1037 |
1038 | ### Custom options
1039 |
1040 | You can customise the options with the `options` option:
1041 |
1042 | ```js
1043 | const options = {
1044 | fields: {
1045 | gender: {
1046 | options: [
1047 | {value: 'M', text: 'Maschio'},
1048 | // use `disabled: true` to disable an option
1049 | {value: 'F', text: 'Femmina', disabled: true}
1050 | ]
1051 | }
1052 | }
1053 | };
1054 | ```
1055 |
1056 | An option is an object with the following structure:
1057 |
1058 | ```js
1059 | {
1060 | value: string, // required
1061 | text: string, // required
1062 | disabled: ?boolean // optional, default = false
1063 | }
1064 | ```
1065 |
1066 | You can also add optgroups:
1067 |
1068 | ```js
1069 | const Car = t.enums.of('Audi Chrysler Ford Renault Peugeot');
1070 |
1071 | const Select = t.struct({
1072 | car: Car
1073 | });
1074 |
1075 | const options = {
1076 | fields: {
1077 | car: {
1078 | options: [
1079 | {value: 'Audi', text: 'Audi'}, // an option
1080 | {label: 'US', options: [ // a group of options
1081 | {value: 'Chrysler', text: 'Chrysler'},
1082 | {value: 'Ford', text: 'Ford'}
1083 | ]},
1084 | {label: 'France', options: [ // another group of options
1085 | {value: 'Renault', text: 'Renault'},
1086 | {value: 'Peugeot', text: 'Peugeot'}
1087 | ], disabled: true} // use `disabled: true` to disable an optgroup
1088 | ]
1089 | }
1090 | }
1091 | };
1092 | ```
1093 |
1094 | ### Render as a radio group
1095 |
1096 | You can render the select as a radio group by using the `factory` option to override the default:
1097 |
1098 | ```js
1099 | const options = {
1100 | factory: t.form.Radio
1101 | };
1102 | ```
1103 |
1104 | ### Multiple select
1105 |
1106 | You can turn the select into a multiple select by passing a `list` as type and using the `factory` option to override the default:
1107 |
1108 | ```js
1109 | const Car = t.enums.of('Audi Chrysler Ford Renault Peugeot');
1110 |
1111 | const Select = t.struct({
1112 | car: t.list(Car)
1113 | });
1114 |
1115 | const options = {
1116 | fields: {
1117 | car: {
1118 | factory: t.form.Select
1119 | }
1120 | }
1121 | };
1122 | ```
1123 |
1124 | ## Date options
1125 |
1126 | The following options are similar to the textbox ones:
1127 |
1128 | - `label`
1129 | - `help`
1130 | - `disabled`
1131 | - `hasError`
1132 | - `error`
1133 | - `template`
1134 |
1135 | ### Fields order
1136 |
1137 | You can sort the fields with the `order` option:
1138 |
1139 | ```js
1140 | const Type = t.struct({
1141 | date: t.Date
1142 | });
1143 |
1144 | const options = {
1145 | fields: {
1146 | date: {
1147 | order: ['D', 'M', 'YY']
1148 | }
1149 | }
1150 | };
1151 | ```
1152 |
1153 | # Customizations
1154 |
1155 | ## Templates
1156 |
1157 | To customise the "skin" of tcomb-form you have to write a *template*. A template is simply a function with the following signature:
1158 |
1159 | ```
1160 | (locals: any) => ReactElement
1161 | ```
1162 |
1163 | where `locals` is an object contaning the "recipe" for rendering the input, and is built by tcomb-form for you.
1164 |
1165 | For example, this is the recipe for a textbox:
1166 |
1167 | ```js
1168 | {
1169 | attrs: Object // should render attributes and events
1170 | config: Object // custom options, see Template addons section
1171 | context: Any // a reference to the context prop
1172 | disabled: maybe(Boolean), // should be disabled
1173 | error: maybe(Label), // should show an error
1174 | hasError: maybe(Boolean), // if true should show an error state
1175 | help: maybe(Label), // should show a help message
1176 | label: maybe(Label), // should show a label
1177 | onChange: Function, // should call this function with the changed value
1178 | path: Array, // the path of this field with respect to the form root
1179 | type: String, // should use this as type attribute
1180 | typeInfo: Object // an object containing info on the current type
1181 | value: Any // the current value of the textbox
1182 | }
1183 | ```
1184 |
1185 | You can set a custom template using the `template` option:
1186 |
1187 | ```js
1188 | const Animal = t.enums({
1189 | dog: "Dog",
1190 | cat: "Cat"
1191 | });
1192 |
1193 | const Pet = t.struct({
1194 | name: t.String,
1195 | type: Animal
1196 | });
1197 |
1198 | const Person = t.struct({
1199 | name: t.String,
1200 | pets: t.list(Pet)
1201 | });
1202 |
1203 | const formLayout = (locals) => {
1204 | return (
1205 |
1206 |
formLayout
1207 |
{locals.inputs.name}
1208 |
{locals.inputs.pets}
1209 |
1210 | );
1211 | };
1212 |
1213 | const petLayout = (locals) => {
1214 | return (
1215 |
1216 |
petLayout
1217 |
{locals.inputs.name}
1218 |
{locals.inputs.type}
1219 |
1220 | );
1221 | };
1222 |
1223 | const options = {
1224 | template: formLayout,
1225 | fields: {
1226 | pets: { // <- pets is a list, you can customise the elements with the `item` option
1227 | item: {
1228 | template: petLayout
1229 | }
1230 | }
1231 | }
1232 | };
1233 |
1234 | const value = {
1235 | name: 'myname',
1236 | pets: [
1237 | {name: 'pet1', type: 'dog'},
1238 | {name: 'pet2', type: 'cat'}
1239 | ]
1240 | };
1241 |
1242 | const App = React.createClass({
1243 |
1244 | save() {
1245 | const value = this.refs.form.getValue();
1246 | if (value) {
1247 | console.log(value);
1248 | }
1249 | },
1250 |
1251 | render() {
1252 |
1253 | return (
1254 |
1255 |
1260 |
1261 |
1262 |
1263 | );
1264 | }
1265 |
1266 | });
1267 | ```
1268 |
1269 | ### Clone default templates
1270 |
1271 | In order to keep the majority of the implementation a template can be cloned. Every template own a series of `render*` function that can be overridden:
1272 |
1273 | ```js
1274 | const Type = t.struct({
1275 | name: t.String
1276 | })
1277 |
1278 | const myTemplate = t.form.Form.templates.textbox.clone({
1279 | // override just the input default implementation (labels, help, error will be preserved)
1280 | renderInput: (locals) => {
1281 | return
1282 | }
1283 | })
1284 |
1285 | const options = {
1286 | fields: {
1287 | name: {
1288 | template: myTemplate
1289 | }
1290 | }
1291 | }
1292 | ```
1293 |
1294 | ## Transformers
1295 |
1296 | Say you want a search textbox which accepts a list of keywords separated by spaces:
1297 |
1298 | ```js
1299 | const Search = t.struct({
1300 | search: t.list(t.String)
1301 | });
1302 | ```
1303 |
1304 | 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:
1305 |
1306 | ```js
1307 | const options = {
1308 | fields: {
1309 | search: {
1310 | factory: t.form.Textbox
1311 | }
1312 | }
1313 | };
1314 | ```
1315 |
1316 | There is a problem though: a textbox handles only strings, so we need a way to transform a list to a string and a string to a list. A `Transformer` deals with serialization / deserialization of data and has the following interface:
1317 |
1318 | ```js
1319 | const Transformer = t.struct({
1320 | format: t.Function, // from value to string, it must be idempotent
1321 | parse: t.Function // from string to value
1322 | });
1323 | ```
1324 |
1325 | **Important**. the `format` function SHOULD BE idempotent.
1326 |
1327 | A basic transformer implementation for the search textbox:
1328 |
1329 | ```js
1330 | const listTransformer = {
1331 | format: (value) => {
1332 | return Array.isArray(value) ? value.join(' ') : value;
1333 | },
1334 | parse: (str) => {
1335 | return str ? str.split(' ') : [];
1336 | }
1337 | };
1338 | ```
1339 |
1340 | Now you can handle lists using the transformer option:
1341 |
1342 | ```js
1343 | // example of initial value
1344 | const value = {
1345 | search: ['climbing', 'yosemite']
1346 | };
1347 |
1348 | const options = {
1349 | fields: {
1350 | search: {
1351 | factory: t.form.Textbox,
1352 | transformer: listTransformer,
1353 | help: 'Keywords are separated by spaces'
1354 | }
1355 | }
1356 | };
1357 | ```
1358 |
1359 | ## Custom factories
1360 |
1361 | The easisiest way to define a custom factory is to extend `t.form.Component` and define the `getTemplate` method:
1362 |
1363 | ```js
1364 | //
1365 | // a custom factory representing a tags input
1366 | //
1367 |
1368 | import React from 'react';
1369 | import t from 'tcomb-form';
1370 | import TagsInput from 'react-tagsinput'; // I'm using this but you can build your own (and reusable!) tagsinput
1371 |
1372 | class TagsComponent extends t.form.Component { // extend the base class
1373 |
1374 | getTemplate() {
1375 | return (locals) => {
1376 | return (
1377 |
1378 | );
1379 | };
1380 | }
1381 |
1382 | }
1383 |
1384 | export default TagsComponent;
1385 | ```
1386 |
1387 | Usage
1388 |
1389 | ```js
1390 | const Type = t.struct({
1391 | tags: t.list(t.String)
1392 | });
1393 |
1394 | const options = {
1395 | fields: {
1396 | tags: {
1397 | factory: TagsComponent
1398 | }
1399 | }
1400 | };
1401 |
1402 | const value = {
1403 | tags: [] // react-tagsinput requires an initial value
1404 | }
1405 | ...
1406 |
1407 |
1413 | ```
1414 |
1415 | ## getTcombFormFactory
1416 |
1417 | If a type owns a `getTcombFormFactory(options)` static function, it will be used to retrieve the suitable factory
1418 |
1419 | **Example**
1420 |
1421 | ```js
1422 | // instead of
1423 | const Country = t.enums.of(['IT', 'US'], 'Country');
1424 |
1425 | const Type = t.struct({
1426 | country: Country
1427 | });
1428 |
1429 | const options = {
1430 | fields: {
1431 | country: {
1432 | factory: t.form.Radio
1433 | }
1434 | }
1435 | };
1436 |
1437 | // you can write
1438 | const Country = t.enums.of(['IT', 'US'], 'Country');
1439 |
1440 | Country.getTcombFormFactory = (/*options*/) => {
1441 | return t.form.Radio;
1442 | };
1443 |
1444 | const Type = t.struct({
1445 | country: Country
1446 | });
1447 |
1448 | const options = {};
1449 | ```
1450 |
1451 | ## getTcombFormOptions
1452 |
1453 | If a type owns a `getTcombFormOptions` static function, tcomb-form will call it and use the result to populate the rendering options.
1454 |
1455 | For example, the following two code snippets behave the same:
1456 |
1457 | ```js
1458 | const Message = t.maybe(t.String);
1459 | const Textbox = t.struct({
1460 | message: Message
1461 | });
1462 |
1463 | Message.getTcombFormOptions = formOptions => ({
1464 | help: Enter text here!
1465 | });
1466 |
1467 |
1468 | ```
1469 |
1470 | ```js
1471 | const Textbox = t.struct({
1472 | message: t.maybe(t.String)
1473 | });
1474 |
1475 | const options = {
1476 | fields: {
1477 | message: {
1478 | help: Enter text here!
1479 | }
1480 | }
1481 | };
1482 |
1483 |
1484 | ```
1485 |
1486 | You can mix and match `Form`'s `options` prop and `getTcombFormOptions` as you see fit. The `formOptions` parameter passed to `getTcombFormOptions` contains the rendering options for this type as passed in via `Form`'s `options` prop, if any.
1487 |
1488 | `getTcombFormOptions` is particularly helpful to keep code easy to follow when using tcomb-form on deeply nested data structures.
1489 |
1490 | # Bootstrap extras
1491 |
1492 | ## Textbox
1493 |
1494 | ### Addons
1495 |
1496 | You can set an addon before or an addon after with the `config.addonBefore` and `config.addonAfter` options:
1497 |
1498 | ```js
1499 | const Textbox = t.struct({
1500 | mytext: t.String
1501 | });
1502 |
1503 | const options = {
1504 | fields: {
1505 | mytext: {
1506 | config: {
1507 | // you can use strings or JSX
1508 | addonBefore: before,
1509 | addonAfter: after
1510 | }
1511 | }
1512 | }
1513 | };
1514 |
1515 |
1516 | ```
1517 |
1518 | You can set a button after (before) with the `config.buttonAfter` (`config.buttonBefore`) option:
1519 |
1520 | ```js
1521 | const options = {
1522 | fields: {
1523 | mytext: {
1524 | config: {
1525 | buttonAfter:
1526 | }
1527 | }
1528 | }
1529 | };
1530 |
1531 |
1532 | ```
1533 |
1534 | ### Size
1535 |
1536 | You can set the textbox size with the `config.size` option:
1537 |
1538 | ```js
1539 | const Textbox = t.struct({
1540 | mytext: t.String
1541 | });
1542 |
1543 | const options = {
1544 | fields: {
1545 | mytext: {
1546 | config: {
1547 | size: 'lg' // xs sm md lg are allowed
1548 | }
1549 | }
1550 | }
1551 | };
1552 |
1553 |
1554 | ```
1555 |
1556 | ## Select
1557 |
1558 | Same as Textbox extras.
1559 |
1560 | ## Struct
1561 |
1562 | You can render the form horizontal with the `config.horizontal` option:
1563 |
1564 | ```js
1565 | const Person = t.struct({
1566 | name: t.String,
1567 | notifyMe: t.Boolean,
1568 | email: t.maybe(t.String)
1569 | });
1570 |
1571 | const options = {
1572 | config: {
1573 | // for each of lg md sm xs you can specify the columns width
1574 | horizontal: {
1575 | md: [3, 9],
1576 | sm: [6, 6]
1577 | }
1578 | }
1579 | };
1580 |
1581 | // remember to add the proper bootstrap style class
1582 | // to a wrapping div (or form) tag in order
1583 | // to get a nice layout
1584 |
1585 |
1586 |
1587 | ```
1588 |
1589 | # General configuration
1590 |
1591 | tcomb-form uses the following default settings:
1592 |
1593 | - English as language
1594 | - Bootstrap as theme ([tcomb-form-templates-bootstrap](https://github.com/gcanti/tcomb-form-templates-bootstrap))
1595 |
1596 | ## Changing the default language
1597 |
1598 | ```js
1599 | import t from 'tcomb-form/lib'; // load tcomb-form without templates and i18n
1600 | import templates from 'tcomb-form-templates-bootstrap';
1601 |
1602 | t.form.Form.templates = templates;
1603 | t.form.Form.i18n = {
1604 | optional: ' (opzionale)',
1605 | required: '',
1606 | add: 'Nuovo',
1607 | remove: 'Elimina',
1608 | up: 'Su',
1609 | down: 'Giù'
1610 | };
1611 | ```
1612 |
1613 | Or pick one in the `i18n` folder (constributions welcome!):
1614 |
1615 | ```js
1616 | import t from 'tcomb-form/lib'; // load tcomb-form without templates and i18n
1617 | import templates from 'tcomb-form-templates-bootstrap';
1618 | import i18n from 'tcomb-form/lib/i18n/it';
1619 |
1620 | t.form.Form.templates = templates;
1621 | t.form.Form.i18n = i18n;
1622 | ```
1623 |
1624 | ## Changing the default skin
1625 |
1626 | ```js
1627 | import t from 'tcomb-form/lib'; // load tcomb-form without templates and i18n
1628 | import i18n from 'tcomb-form/lib/i18n/en';
1629 | import semantic from 'tcomb-form-templates-semantic';
1630 |
1631 | t.form.Form.i18n = i18n;
1632 | t.form.Form.templates = semantic;
1633 | ```
1634 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Version
2 |
3 | Tell us which versions you are using:
4 |
5 | - tcomb-form v0.8.?
6 | - react v0.14.?
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) 2014 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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/gcanti/tcomb-form)
2 | [](https://david-dm.org/gcanti/tcomb-form)
3 | 
4 |
5 | > "Simplicity is the ultimate sophistication" (Leonardo da Vinci)
6 |
7 | # Notice
8 |
9 | `tcomb-form` 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, reviewing and testing some PRs.
10 |
11 | # Domain Driven Forms
12 |
13 | The [tcomb library](https://github.com/gcanti/tcomb) provides a concise but expressive way to define domain models in JavaScript.
14 |
15 | The [tcomb-validation library](https://github.com/gcanti/tcomb-validation) builds on tcomb, providing validation functions for tcomb domain models.
16 |
17 | This library builds on those two and **realizes an old dream of mine**.
18 |
19 | # Playground
20 |
21 | This [playground](https://gcanti.github.io/resources/tcomb-form/playground/playground.html), while a bit outdated, gives you the general idea.
22 |
23 | # Benefits
24 |
25 | With tcomb-form you simply call `` to generate a form based on that domain model. What does this get you?
26 |
27 | 1. Write a lot less HTML
28 | 2. Usability and accessibility for free (automatic labels, inline validation, etc)
29 | 3. No need to update forms when domain model changes
30 |
31 | # Flexibility
32 |
33 | - tcomb-forms lets you override automatic features or add additional information to forms.
34 | - You often don't want to use your domain model directly for a form. You can easily create a form specific model with tcomb that captures the details of a particular feature, and then define a function that uses that model to process the main domain model.
35 |
36 | # Example
37 |
38 | ```js
39 | import t from 'tcomb-form'
40 |
41 | const FormSchema = t.struct({
42 | name: t.String, // a required string
43 | age: t.maybe(t.Number), // an optional number
44 | rememberMe: t.Boolean // a boolean
45 | })
46 |
47 | const App = React.createClass({
48 |
49 | onSubmit(evt) {
50 | evt.preventDefault()
51 | const value = this.refs.form.getValue()
52 | if (value) {
53 | console.log(value)
54 | }
55 | },
56 |
57 | render() {
58 | return (
59 |
65 | )
66 | }
67 |
68 | })
69 | ```
70 |
71 | **Output**. Labels are automatically generated.
72 |
73 | 
74 |
75 | # Documentation
76 |
77 | [GUIDE.md](GUIDE.md)
78 |
79 | **Browser compatibility**: same as React >=0.13.0
80 |
81 | # Contributions
82 |
83 | Thanks so much to [Chris Pearce](https://github.com/Chrisui) for pointing me in the right direction
84 | and for supporting me in the v0.4 rewrite.
85 |
86 | Special thanks to [William Lubelski](https://github.com/lubelski) ([@uiwill](https://twitter.com/uiwill)), without him this library would be less magic.
87 |
88 | Thanks to [Esa-Matti Suuronen](https://github.com/epeli) for the excellent `humanize()` function.
89 |
90 | Thanks to [Andrey Popp](https://github.com/andreypopp) for writing [react-forms](https://github.com/prometheusresearch/react-forms), great inspiration for list management.
91 |
92 | # Contributing
93 |
94 | [CONTRIBUTING.md](CONTRIBUTING.md)
95 |
--------------------------------------------------------------------------------
/docs/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gcanti/tcomb-form/94ae91864b6379dae8e517d9a57c2e2be5b86a0f/docs/example.png
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack') // eslint-disable-line
2 | var path = require('path') // eslint-disable-line
3 |
4 | module.exports = function getConfig(config) {
5 | config.set({
6 | browserNoActivityTimeout: 30000,
7 | browsers: [process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome'],
8 | singleRun: true,
9 | frameworks: ['tap'],
10 | files: [
11 | 'test/index.js'
12 | ],
13 | preprocessors: {
14 | 'test/index.js': ['webpack']
15 | },
16 | webpack: {
17 | node: {
18 | fs: 'empty'
19 | },
20 | module: {
21 | preLoaders: [
22 | {
23 | test: /\.js$/,
24 | exclude: [
25 | path.resolve('src/'),
26 | path.resolve('node_modules/')
27 | ],
28 | loader: 'babel'
29 | },
30 | {
31 | test: /\.js$/,
32 | include: path.resolve('src/'),
33 | loader: 'isparta'
34 | }
35 | ]
36 | },
37 | plugins: [
38 | new webpack.DefinePlugin({
39 | 'process.env.NODE_ENV': JSON.stringify('test')
40 | })
41 | ]
42 | },
43 | webpackMiddleware: {
44 | noInfo: true
45 | },
46 | reporters: ['dots', 'coverage'],
47 | coverageReporter: {
48 | type: 'text'
49 | }
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tcomb-form",
3 | "version": "0.9.21",
4 | "description": "React.js powered UI library for developing forms writing less code",
5 | "main": "lib/main.js",
6 | "files": [
7 | "lib",
8 | "src",
9 | "dist"
10 | ],
11 | "scripts": {
12 | "dev": "webpack --config=dev/webpack.config.js --watch --progress",
13 | "lint": "eslint src test",
14 | "test:node": "babel-node test",
15 | "test": "npm run lint && karma start",
16 | "watch": "rm -rf lib/* && babel src -d lib -w",
17 | "build": "rm -rf lib/* && babel src -d lib",
18 | "dist": "webpack --config webpack.dist.config.js --progress"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/gcanti/tcomb-form.git"
23 | },
24 | "author": "Giulio Canti ",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/gcanti/tcomb-form/issues"
28 | },
29 | "homepage": "https://github.com/gcanti/tcomb-form",
30 | "dependencies": {
31 | "tcomb-form-templates-bootstrap": "^0.2.0",
32 | "tcomb-validation": "^3.0.0"
33 | },
34 | "peerDependencies": {
35 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0"
36 | },
37 | "devDependencies": {
38 | "babel": "5.8.38",
39 | "babel-core": "5.8.38",
40 | "babel-eslint": "4.1.8",
41 | "babel-loader": "5.3.2",
42 | "eslint": "1.10.3",
43 | "eslint-config-airbnb": "1.0.0",
44 | "eslint-plugin-react": "3.10.0",
45 | "isparta-loader": "1.0.0",
46 | "karma": "2.0.0",
47 | "karma-chrome-launcher": "2.2.0",
48 | "karma-coverage": "1.1.1",
49 | "karma-firefox-launcher": "1.1.0",
50 | "karma-tap": "3.1.1",
51 | "karma-webpack": "1.7.0",
52 | "react": "^15.0.0",
53 | "react-dom": "^15.0.0",
54 | "react-test-renderer": "^15.5.4",
55 | "tape": "4.0.0",
56 | "webpack": "1.12.8"
57 | },
58 | "tags": [
59 | "form",
60 | "forms",
61 | "validation",
62 | "generation",
63 | "react",
64 | "react-component"
65 | ],
66 | "keywords": [
67 | "form",
68 | "forms",
69 | "validation",
70 | "generation",
71 | "react",
72 | "react-component"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/src/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 | isArraysShallowDiffers
13 | } from './util'
14 |
15 | const Nil = t.Nil
16 | const assert = t.assert
17 | const SOURCE = 'tcomb-form'
18 | const noobj = Object.freeze({})
19 | const noarr = Object.freeze([])
20 | const noop = () => {}
21 |
22 | function getFormComponent(type, options) {
23 | if (options.factory) {
24 | return options.factory
25 | }
26 | if (type.getTcombFormFactory) {
27 | return type.getTcombFormFactory(options)
28 | }
29 | const name = t.getTypeName(type)
30 | switch (type.meta.kind) {
31 | case 'irreducible' :
32 | if (type === t.Boolean) {
33 | return Checkbox // eslint-disable-line no-use-before-define
34 | } else if (type === t.Date) {
35 | return Datetime // eslint-disable-line no-use-before-define
36 | }
37 | return Textbox // eslint-disable-line no-use-before-define
38 | case 'struct' :
39 | case 'interface' :
40 | return Struct // eslint-disable-line no-use-before-define
41 | case 'list' :
42 | return List // eslint-disable-line no-use-before-define
43 | case 'enums' :
44 | return Select // eslint-disable-line no-use-before-define
45 | case 'maybe' :
46 | case 'subtype' :
47 | return getFormComponent(type.meta.type, options)
48 | default :
49 | t.fail(`[${SOURCE}] unsupported kind ${type.meta.kind} for type ${name}`)
50 | }
51 | }
52 |
53 | exports.getComponent = getFormComponent
54 |
55 | function sortByText(a, b) {
56 | return a.text.localeCompare(b.text)
57 | }
58 |
59 | function getComparator(order) {
60 | return {
61 | asc: sortByText,
62 | desc: (a, b) => -sortByText(a, b)
63 | }[order]
64 | }
65 |
66 | export const decorators = {
67 |
68 | template(name) {
69 | return (Component) => {
70 | Component.prototype.getTemplate = function getTemplate() {
71 | return this.props.options.template || this.props.ctx.templates[name]
72 | }
73 | }
74 | },
75 |
76 | attrs(Component) {
77 | Component.prototype.getAttrs = function getAttrs() {
78 | const attrs = t.mixin({}, this.props.options.attrs)
79 | attrs.id = this.getId()
80 | attrs.name = this.getName()
81 | return attrs
82 | }
83 | },
84 |
85 | templates(Component) {
86 | Component.prototype.getTemplates = function getTemplates() {
87 | return merge(this.props.ctx.templates, this.props.options.templates)
88 | }
89 | }
90 |
91 | }
92 |
93 | export class Component extends React.Component {
94 |
95 | static transformer = {
96 | format: value => Nil.is(value) ? null : value,
97 | parse: value => value
98 | }
99 |
100 | constructor(props) {
101 | super(props)
102 | this.typeInfo = getTypeInfo(props.type)
103 | this.state = {
104 | isPristine: true,
105 | hasError: false,
106 | value: this.getTransformer().format(props.value)
107 | }
108 | }
109 |
110 | getTransformer() {
111 | return this.props.options.transformer || this.constructor.transformer
112 | }
113 |
114 | shouldComponentUpdate(nextProps, nextState) {
115 | const { props, state } = this
116 | const nextPath = Boolean(nextProps.ctx) && nextProps.ctx.path
117 | const curPath = Boolean(props.ctx) && props.ctx.path
118 |
119 | const should = (
120 | nextState.value !== state.value ||
121 | nextState.hasError !== state.hasError ||
122 | nextProps.options !== props.options ||
123 | nextProps.type !== props.type ||
124 | isArraysShallowDiffers(nextPath, curPath)
125 | )
126 |
127 | return should
128 | }
129 |
130 | componentWillReceiveProps(props) {
131 | if (props.type !== this.props.type) {
132 | this.typeInfo = getTypeInfo(props.type)
133 | }
134 | const value = this.getTransformer().format(props.value)
135 | this.setState({ value })
136 | }
137 |
138 | onChange = (value) => {
139 | this.setState({ value, isPristine: false }, () => {
140 | this.props.onChange(value, this.props.ctx.path)
141 | })
142 | }
143 |
144 | getValidationOptions() {
145 | const context = this.props.context || this.props.ctx.context
146 | return {
147 | path: this.props.ctx.path,
148 | context: t.mixin(t.mixin({}, context), { options: this.props.options })
149 | }
150 | }
151 |
152 | getValue() {
153 | return this.getTransformer().parse(this.state.value)
154 | }
155 |
156 | isValueNully() {
157 | return Nil.is(this.getValue())
158 | }
159 |
160 | removeErrors() {
161 | this.setState({ hasError: false })
162 | }
163 |
164 | validate() {
165 | const result = t.validate(this.getValue(), this.props.type, this.getValidationOptions())
166 | this.setState({ hasError: !result.isValid() })
167 | return result
168 | }
169 |
170 | getAuto() {
171 | return this.props.options.auto || this.props.ctx.auto
172 | }
173 |
174 | getI18n() {
175 | return this.props.options.i18n || this.props.ctx.i18n
176 | }
177 |
178 | getDefaultLabel() {
179 | const label = this.props.ctx.label
180 | if (label) {
181 | const suffix = this.typeInfo.isMaybe ? this.getI18n().optional : this.getI18n().required
182 | return label + suffix
183 | }
184 | }
185 |
186 | getLabel() {
187 | let label = this.props.options.label || this.props.options.legend
188 | if (Nil.is(label) && this.getAuto() === 'labels') {
189 | label = this.getDefaultLabel()
190 | }
191 | return label
192 | }
193 |
194 | getError() {
195 | if (this.hasError()) {
196 | const error = this.props.options.error || this.typeInfo.getValidationErrorMessage
197 | if (t.Function.is(error)) {
198 | const { path, context } = this.getValidationOptions()
199 | return error(this.getValue(), path, context)
200 | }
201 | return error
202 | }
203 | }
204 |
205 | hasError() {
206 | return this.props.options.hasError || this.state.hasError
207 | }
208 |
209 | getConfig() {
210 | return merge(this.props.ctx.config, this.props.options.config)
211 | }
212 |
213 | getId() {
214 | const attrs = this.props.options.attrs || noobj
215 | if (attrs.id) {
216 | return attrs.id
217 | }
218 | if (!this.uid) {
219 | this.uid = this.props.ctx.uidGenerator.next()
220 | }
221 | return this.uid
222 | }
223 |
224 | getName() {
225 | return this.props.options.name || this.props.ctx.name || this.getId()
226 | }
227 |
228 | getLocals() {
229 | const options = this.props.options
230 | const value = this.state.value
231 | return {
232 | typeInfo: this.typeInfo,
233 | path: this.props.ctx.path,
234 | isPristine: this.state.isPristine,
235 | error: this.getError(),
236 | hasError: this.hasError(),
237 | label: this.getLabel(),
238 | onChange: this.onChange,
239 | config: this.getConfig(),
240 | value,
241 | disabled: options.disabled,
242 | help: options.help,
243 | context: this.props.ctx.context
244 | }
245 | }
246 |
247 | render() {
248 | const locals = this.getLocals()
249 | if (process.env.NODE_ENV !== 'production') {
250 | // getTemplate is the only required implementation when extending Component
251 | assert(t.Function.is(this.getTemplate), `[${SOURCE}] missing getTemplate method of component ${this.constructor.name}`)
252 | }
253 | const template = this.getTemplate()
254 | return template(locals)
255 | }
256 |
257 | }
258 |
259 | function toNull(value) {
260 | return (t.String.is(value) && value.trim() === '') || Nil.is(value) ? null : value
261 | }
262 |
263 | function parseNumber(value) {
264 | const n = parseFloat(value)
265 | const isNumeric = value == n // eslint-disable-line eqeqeq
266 | return isNumeric ? n : toNull(value)
267 | }
268 |
269 | @decorators.attrs
270 | @decorators.template('textbox')
271 | export class Textbox extends Component {
272 |
273 | static transformer = {
274 | format: value => Nil.is(value) ? '' : value,
275 | parse: toNull
276 | }
277 |
278 | static numberTransformer = {
279 | format: value => Nil.is(value) ? '' : String(value),
280 | parse: parseNumber
281 | }
282 |
283 | getTransformer() {
284 | const options = this.props.options
285 | if (options.transformer) {
286 | return options.transformer
287 | } else if (this.typeInfo.innerType === t.Number) {
288 | return Textbox.numberTransformer
289 | }
290 | return Textbox.transformer
291 | }
292 |
293 | getPlaceholder() {
294 | const attrs = this.props.options.attrs || noobj
295 | let placeholder = attrs.placeholder
296 | if (Nil.is(placeholder) && this.getAuto() === 'placeholders') {
297 | placeholder = this.getDefaultLabel()
298 | }
299 | return placeholder
300 | }
301 |
302 | getLocals() {
303 | const locals = super.getLocals()
304 | locals.attrs = this.getAttrs()
305 | locals.attrs.placeholder = this.getPlaceholder()
306 | locals.type = this.props.options.type || 'text'
307 | return locals
308 | }
309 |
310 | }
311 |
312 | @decorators.attrs
313 | @decorators.template('checkbox')
314 | export class Checkbox extends Component {
315 |
316 | static transformer = {
317 | format: value => Nil.is(value) ? false : value,
318 | parse: value => value
319 | }
320 |
321 | getLocals() {
322 | const locals = super.getLocals()
323 | locals.attrs = this.getAttrs()
324 | // checkboxes must always have a label
325 | locals.label = locals.label || this.getDefaultLabel()
326 | return locals
327 | }
328 |
329 | }
330 |
331 | @decorators.attrs
332 | @decorators.template('select')
333 | export class Select extends Component {
334 |
335 | static transformer = (nullOption) => {
336 | return {
337 | format: value => Nil.is(value) && nullOption ? nullOption.value : value,
338 | parse: value => nullOption && nullOption.value === value ? null : value
339 | }
340 | }
341 |
342 | static multipleTransformer = {
343 | format: value => Nil.is(value) ? noarr : value,
344 | parse: value => value
345 | }
346 |
347 | getTransformer() {
348 | const options = this.props.options
349 | if (options.transformer) {
350 | return options.transformer
351 | }
352 | if (this.isMultiple()) {
353 | return Select.multipleTransformer
354 | }
355 | return Select.transformer(this.getNullOption())
356 | }
357 |
358 | getNullOption() {
359 | return this.props.options.nullOption || {value: '', text: '-'}
360 | }
361 |
362 | isMultiple() {
363 | return this.typeInfo.innerType.meta.kind === 'list'
364 | }
365 |
366 | getEnum() {
367 | return this.isMultiple() ? getTypeInfo(this.typeInfo.innerType.meta.type).innerType : this.typeInfo.innerType
368 | }
369 |
370 | getOptions() {
371 | const options = this.props.options
372 | const items = options.options ? options.options.slice() : getOptionsOfEnum(this.getEnum())
373 | if (options.order) {
374 | items.sort(getComparator(options.order))
375 | }
376 | const nullOption = this.getNullOption()
377 | if (!this.isMultiple() && options.nullOption !== false) {
378 | items.unshift(nullOption)
379 | }
380 | return items
381 | }
382 |
383 | getLocals() {
384 | const locals = super.getLocals()
385 | locals.attrs = this.getAttrs()
386 | locals.options = this.getOptions()
387 | locals.isMultiple = this.isMultiple()
388 | return locals
389 | }
390 |
391 | }
392 |
393 | @decorators.attrs
394 | @decorators.template('radio')
395 | export class Radio extends Component {
396 |
397 | static transformer = {
398 | format: value => Nil.is(value) ? null : value,
399 | parse: value => value
400 | }
401 |
402 | getOptions() {
403 | const options = this.props.options
404 | const items = options.options ? options.options.slice() : getOptionsOfEnum(this.typeInfo.innerType)
405 | if (options.order) {
406 | items.sort(getComparator(options.order))
407 | }
408 | return items
409 | }
410 |
411 | getLocals() {
412 | const locals = super.getLocals()
413 | locals.attrs = this.getAttrs()
414 | locals.options = this.getOptions()
415 | return locals
416 | }
417 |
418 | }
419 |
420 | const defaultDatetimeValue = Object.freeze([null, null, null])
421 |
422 | @decorators.attrs
423 | @decorators.template('date')
424 | export class Datetime extends Component {
425 |
426 | static transformer = {
427 | format: (value) => {
428 | if (t.Array.is(value)) {
429 | return value
430 | } else if (t.Date.is(value)) {
431 | return [value.getFullYear(), value.getMonth(), value.getDate()].map(String)
432 | }
433 | return defaultDatetimeValue
434 | },
435 | parse: (value) => {
436 | const numbers = value.map(parseNumber)
437 | if (numbers.every(t.Number.is)) {
438 | return new Date(numbers[0], numbers[1], numbers[2])
439 | } else if (numbers.every(Nil.is)) {
440 | return null
441 | }
442 | return numbers
443 | }
444 | }
445 |
446 | getOrder() {
447 | return this.props.options.order || ['M', 'D', 'YY']
448 | }
449 |
450 | getLocals() {
451 | const locals = super.getLocals()
452 | locals.attrs = this.getAttrs()
453 | locals.order = this.getOrder()
454 | return locals
455 | }
456 |
457 | }
458 |
459 | class ComponentWithChildRefs extends Component {
460 |
461 | childRefs = {};
462 |
463 | setChildRefFor = prop => ref => {
464 | if (ref) {
465 | this.childRefs[prop] = ref
466 | } else {
467 | delete this.childRefs[prop]
468 | }
469 | }
470 |
471 | }
472 |
473 | @decorators.templates
474 | export class Struct extends ComponentWithChildRefs {
475 |
476 | static transformer = {
477 | format: value => Nil.is(value) ? noobj : value,
478 | parse: value => value
479 | }
480 |
481 | isValueNully() {
482 | return Object.keys(this.childRefs).every((key) => this.childRefs[key].isValueNully())
483 | }
484 |
485 | removeErrors() {
486 | this.setState({ hasError: false })
487 | Object.keys(this.childRefs).forEach((key) => this.childRefs[key].removeErrors())
488 | }
489 |
490 | validate() {
491 | let value = {}
492 | let errors = []
493 | let result
494 |
495 | if (this.typeInfo.isMaybe && this.isValueNully()) {
496 | this.removeErrors()
497 | return new t.ValidationResult({errors: [], value: null})
498 | }
499 |
500 | const props = this.getTypeProps()
501 | for (const ref in props) {
502 | if (this.childRefs.hasOwnProperty(ref)) {
503 | result = this.childRefs[ref].validate()
504 | errors = errors.concat(result.errors)
505 | value[ref] = result.value
506 | }
507 | }
508 |
509 | if (errors.length === 0) {
510 | const InnerType = this.typeInfo.innerType
511 | value = this.getTransformer().parse(value)
512 | value = new InnerType(value)
513 | if (this.typeInfo.isSubtype) {
514 | result = t.validate(value, this.props.type, this.getValidationOptions())
515 | errors = result.errors
516 | }
517 | }
518 |
519 | this.setState({ hasError: errors.length > 0 })
520 | return new t.ValidationResult({errors, value})
521 | }
522 |
523 | onChange = (fieldName, fieldValue, path, kind) => {
524 | const value = t.mixin({}, this.state.value)
525 | value[fieldName] = fieldValue
526 | this.setState({ value, isPristine: false }, () => {
527 | this.props.onChange(value, path, kind)
528 | })
529 | }
530 |
531 | getTemplate() {
532 | return this.props.options.template || this.getTemplates().struct
533 | }
534 |
535 | getTypeProps() {
536 | return this.typeInfo.innerType.meta.props
537 | }
538 |
539 | getOrder() {
540 | return this.props.options.order || Object.keys(this.getTypeProps())
541 | }
542 |
543 | getInputs() {
544 | const { options, ctx } = this.props
545 | const props = this.getTypeProps()
546 | const auto = this.getAuto()
547 | const i18n = this.getI18n()
548 | const config = this.getConfig()
549 | const templates = this.getTemplates()
550 | const value = this.state.value
551 | const inputs = {}
552 |
553 | for (const prop in props) {
554 | if (props.hasOwnProperty(prop)) {
555 | const type = props[prop]
556 | const propValue = value[prop]
557 | const propType = getTypeFromUnion(type, propValue)
558 | const fieldsOptions = options.fields || noobj
559 | const propOptions = getComponentOptions(fieldsOptions[prop], noobj, propValue, type)
560 | inputs[prop] = React.createElement(getFormComponent(propType, propOptions), {
561 | key: prop,
562 | ref: this.setChildRefFor(prop),
563 | type: propType,
564 | options: propOptions,
565 | value: propValue,
566 | onChange: this.onChange.bind(this, prop),
567 | ctx: {
568 | context: ctx.context,
569 | uidGenerator: ctx.uidGenerator,
570 | auto,
571 | config,
572 | name: ctx.name ? `${ctx.name}[${prop}]` : prop,
573 | label: humanize(prop),
574 | i18n,
575 | templates,
576 | path: ctx.path.concat(prop)
577 | }
578 | })
579 | }
580 | }
581 | return inputs
582 | }
583 |
584 | getLocals() {
585 | const options = this.props.options
586 | const locals = super.getLocals()
587 | locals.order = this.getOrder()
588 | locals.inputs = this.getInputs()
589 | locals.className = options.className
590 | return locals
591 | }
592 |
593 | }
594 |
595 | function toSameLength(value, keys, uidGenerator) {
596 | if (value.length === keys.length) {
597 | return keys
598 | }
599 | const ret = []
600 | for (let i = 0, len = value.length; i < len; i++ ) {
601 | ret[i] = keys[i] || uidGenerator.next()
602 | }
603 | return ret
604 | }
605 |
606 | @decorators.templates
607 | export class List extends ComponentWithChildRefs {
608 |
609 | static transformer = {
610 | format: value => Nil.is(value) ? noarr : value,
611 | parse: value => value
612 | }
613 |
614 | constructor(props) {
615 | super(props)
616 | this.state.keys = this.state.value.map(() => props.ctx.uidGenerator.next())
617 | }
618 |
619 | componentWillReceiveProps(props) {
620 | if (props.type !== this.props.type) {
621 | this.typeInfo = getTypeInfo(props.type)
622 | }
623 | const value = this.getTransformer().format(props.value)
624 | this.setState({
625 | value,
626 | keys: toSameLength(value, this.state.keys, props.ctx.uidGenerator)
627 | })
628 | }
629 |
630 | isValueNully() {
631 | return this.state.value.length === 0
632 | }
633 |
634 | removeErrors() {
635 | this.setState({ hasError: false })
636 | Object.keys(this.childRefs).forEach((key) => this.childRefs[key].removeErrors())
637 | }
638 |
639 | validate() {
640 | let value = []
641 | let errors = []
642 | let result
643 |
644 | if (this.typeInfo.isMaybe && this.isValueNully()) {
645 | this.removeErrors()
646 | return new t.ValidationResult({errors: [], value: null})
647 | }
648 |
649 | for (let i = 0, len = this.state.value.length; i < len; i++ ) {
650 | result = this.childRefs[i].validate()
651 | errors = errors.concat(result.errors)
652 | value.push(result.value)
653 | }
654 |
655 | // handle subtype
656 | if (this.typeInfo.isSubtype && errors.length === 0) {
657 | value = this.getTransformer().parse(value)
658 | result = t.validate(value, this.props.type, this.getValidationOptions())
659 | errors = result.errors
660 | }
661 |
662 | this.setState({ hasError: errors.length > 0 })
663 | return new t.ValidationResult({errors: errors, value: value})
664 | }
665 |
666 | onChange = (value, keys, path, kind) => {
667 | const allkeys = toSameLength(value, keys, this.props.ctx.uidGenerator)
668 | this.setState({ value, keys: allkeys, isPristine: false }, () => {
669 | this.props.onChange(value, path, kind)
670 | })
671 | }
672 |
673 | addItem = () => {
674 | const value = this.state.value.concat(undefined)
675 | const keys = this.state.keys.concat(this.props.ctx.uidGenerator.next())
676 | this.onChange(value, keys, this.props.ctx.path.concat(value.length - 1), 'add')
677 | }
678 |
679 | onItemChange(itemIndex, itemValue, path, kind) {
680 | const value = this.state.value.slice()
681 | value[itemIndex] = itemValue
682 | this.onChange(value, this.state.keys, path, kind)
683 | }
684 |
685 | removeItem(i) {
686 | const value = this.state.value.slice()
687 | value.splice(i, 1)
688 | const keys = this.state.keys.slice()
689 | keys.splice(i, 1)
690 | this.onChange(value, keys, this.props.ctx.path.concat(i), 'remove')
691 | }
692 |
693 | moveUpItem(i) {
694 | if (i > 0) {
695 | this.onChange(
696 | move(this.state.value.slice(), i, i - 1),
697 | move(this.state.keys.slice(), i, i - 1),
698 | this.props.ctx.path.concat(i),
699 | 'moveUp'
700 | )
701 | }
702 | }
703 |
704 | moveDownItem(i) {
705 | if (i < this.state.value.length - 1) {
706 | this.onChange(
707 | move(this.state.value.slice(), i, i + 1),
708 | move(this.state.keys.slice(), i, i + 1),
709 | this.props.ctx.path.concat(i),
710 | 'moveDown'
711 | )
712 | }
713 | }
714 |
715 | getTemplate() {
716 | return this.props.options.template || this.getTemplates().list
717 | }
718 |
719 | getItems() {
720 | const { options, ctx } = this.props
721 | const auto = this.getAuto()
722 | const i18n = this.getI18n()
723 | const config = this.getConfig()
724 | const templates = this.getTemplates()
725 | const value = this.state.value
726 | return value.map((itemValue, i) => {
727 | const type = this.typeInfo.innerType.meta.type
728 | const itemType = getTypeFromUnion(type, itemValue)
729 | const itemOptions = getComponentOptions(options.item, noobj, itemValue, type)
730 | const ItemComponent = getFormComponent(itemType, itemOptions)
731 | const buttons = []
732 | if (!options.disableRemove) {
733 | buttons.push({
734 | type: 'remove',
735 | label: i18n.remove,
736 | click: this.removeItem.bind(this, i)
737 | })
738 | }
739 | if (!options.disableOrder) {
740 | buttons.push({
741 | type: 'move-up',
742 | label: i18n.up,
743 | click: this.moveUpItem.bind(this, i)
744 | })
745 | }
746 | if (!options.disableOrder) {
747 | buttons.push({
748 | type: 'move-down',
749 | label: i18n.down,
750 | click: this.moveDownItem.bind(this, i)
751 | })
752 | }
753 | return {
754 | input: React.createElement(ItemComponent, {
755 | ref: this.setChildRefFor(i),
756 | type: itemType,
757 | options: itemOptions,
758 | value: itemValue,
759 | onChange: this.onItemChange.bind(this, i),
760 | ctx: {
761 | context: ctx.context,
762 | uidGenerator: ctx.uidGenerator,
763 | auto,
764 | config,
765 | i18n,
766 | name: ctx.name ? `${ctx.name}[${i}]` : String(i),
767 | templates,
768 | path: ctx.path.concat(i)
769 | }
770 | }),
771 | key: this.state.keys[i],
772 | buttons: buttons
773 | }
774 | })
775 | }
776 |
777 | getLocals() {
778 | const options = this.props.options
779 | const i18n = this.getI18n()
780 | const locals = super.getLocals()
781 | locals.add = options.disableAdd ? null : {
782 | type: 'add',
783 | label: i18n.add,
784 | click: this.addItem
785 | }
786 | locals.items = this.getItems()
787 | locals.className = options.className
788 | return locals
789 | }
790 |
791 | }
792 |
793 | export class Form extends React.Component {
794 | inputRef = null
795 |
796 | setInputRef = ref => {
797 | this.inputRef = ref
798 | }
799 |
800 | validate() {
801 | return this.inputRef.validate()
802 | }
803 |
804 | getValue() {
805 | const result = this.validate()
806 | return result.isValid() ? result.value : null
807 | }
808 |
809 | getComponent(path) {
810 | const points = t.String.is(path) ? path.split('.') : path
811 | return points.reduce((input, name) => input.childRefs[name], this.inputRef)
812 | }
813 |
814 | getSeed() {
815 | const rii = this._reactInternalInstance
816 | if (rii) {
817 | if (rii._hostContainerInfo) {
818 | return rii._hostContainerInfo._idCounter
819 | }
820 | if (rii._nativeContainerInfo) {
821 | return rii._nativeContainerInfo._idCounter
822 | }
823 | if (rii._rootNodeID) {
824 | return rii._rootNodeID
825 | }
826 | }
827 | return '0'
828 | }
829 |
830 | getUIDGenerator() {
831 | this.uidGenerator = this.uidGenerator || new UIDGenerator(this.getSeed())
832 | return this.uidGenerator
833 | }
834 |
835 | render() {
836 | const { i18n, templates } = Form
837 |
838 | if (process.env.NODE_ENV !== 'production') {
839 | assert(t.isType(this.props.type), `[${SOURCE}] missing required prop type`)
840 | assert(t.maybe(t.Object).is(this.props.options) || t.Function.is(this.props.options) || t.list(t.maybe(t.Object)).is(this.props.options), `[${SOURCE}] prop options, if specified, must be an object, a function returning the options or a list of options for unions`)
841 | assert(t.Object.is(templates), `[${SOURCE}] missing templates config`)
842 | assert(t.Object.is(i18n), `[${SOURCE}] missing i18n config`)
843 | }
844 |
845 | const value = this.props.value
846 | const type = getTypeFromUnion(this.props.type, value)
847 | const options = getComponentOptions(this.props.options, noobj, value, this.props.type)
848 |
849 | // this is in the render method because I need this._reactInternalInstance._rootNodeID in React ^0.14.0
850 | // and this._reactInternalInstance._nativeContainerInfo._idCounter in React ^15.0.0
851 | const uidGenerator = this.getUIDGenerator()
852 |
853 | return React.createElement(getFormComponent(type, options), {
854 | ref: this.setInputRef,
855 | type: type,
856 | options,
857 | value: value,
858 | onChange: this.props.onChange || noop,
859 | ctx: this.props.ctx || {
860 | context: this.props.context,
861 | uidGenerator,
862 | auto: 'labels',
863 | templates,
864 | i18n,
865 | path: []
866 | }
867 | })
868 | }
869 |
870 | }
871 |
--------------------------------------------------------------------------------
/src/i18n/de.js:
--------------------------------------------------------------------------------
1 | export default {
2 | optional: ' (optional)',
3 | required: '',
4 | add: 'Hinzufügen',
5 | remove: 'Entfernen',
6 | up: 'Hoch',
7 | down: 'Runter'
8 | }
9 |
--------------------------------------------------------------------------------
/src/i18n/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | optional: ' (optional)',
3 | required: '',
4 | add: 'Add',
5 | remove: 'Remove',
6 | up: 'Up',
7 | down: 'Down'
8 | }
9 |
--------------------------------------------------------------------------------
/src/i18n/es.js:
--------------------------------------------------------------------------------
1 | export default {
2 | optional: ' (opcional)',
3 | required: '',
4 | add: 'Añadir',
5 | remove: 'Eliminar',
6 | up: 'Arriba',
7 | down: 'Abajo'
8 | }
9 |
--------------------------------------------------------------------------------
/src/i18n/fr.js:
--------------------------------------------------------------------------------
1 | export default {
2 | optional: ' (optionnel)',
3 | required: '',
4 | add: 'Ajouter',
5 | remove: 'Supprimer',
6 | up: 'Monter',
7 | down: 'Descendre'
8 | }
9 |
--------------------------------------------------------------------------------
/src/i18n/it.js:
--------------------------------------------------------------------------------
1 | export default {
2 | optional: ' (opzionale)',
3 | required: '',
4 | add: 'Nuovo',
5 | remove: 'Elimina',
6 | up: 'Su',
7 | down: 'Giù'
8 | }
9 |
--------------------------------------------------------------------------------
/src/i18n/uk.js:
--------------------------------------------------------------------------------
1 | export default {
2 | optional: ' (необов\'язкове)',
3 | required: '',
4 | add: 'Додати',
5 | remove: 'Видалити',
6 | up: 'Вверх',
7 | down: 'Вниз'
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* global File */
2 | import t from 'tcomb-validation'
3 | import * as components from './components'
4 |
5 | t.form = components
6 | t.form.File = t.irreducible('File', x => x instanceof File)
7 |
8 | module.exports = t
9 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /*! @preserve
2 | *
3 | * The MIT License (MIT)
4 | *
5 | * Copyright (c) 2014 Giulio Canti
6 | *
7 | */
8 | import t from './index.js'
9 | import templates from 'tcomb-form-templates-bootstrap'
10 | import i18n from './i18n/en'
11 |
12 | t.form.Form.templates = templates
13 | t.form.Form.i18n = i18n
14 |
15 | export default t
16 |
--------------------------------------------------------------------------------
/src/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 ? (value, path, context) => {
39 | const result = t.validate(value, type, {path, context})
40 | if (!result.isValid()) {
41 | for (let i = 0, len = result.errors.length; i < len; i++ ) {
42 | if (t.Function.is(result.errors[i].expected.getValidationErrorMessage)) {
43 | return result.errors[i].message
44 | }
45 | }
46 | return innerGetValidationErrorMessage(value, path, context)
47 | }
48 | } : undefined
49 |
50 | return {
51 | type,
52 | isMaybe,
53 | isSubtype,
54 | innerType,
55 | getValidationErrorMessage
56 | }
57 | }
58 |
59 | // thanks to https://github.com/epeli/underscore.string
60 |
61 | function underscored(s) {
62 | return s.trim().replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase()
63 | }
64 |
65 | function capitalize(s) {
66 | return s.charAt(0).toUpperCase() + s.slice(1)
67 | }
68 |
69 | export function humanize(s) {
70 | return capitalize(underscored(s).replace(/_id$/, '').replace(/_/g, ' '))
71 | }
72 |
73 | export function merge(a, b) {
74 | return mixin(mixin({}, a), b, true)
75 | }
76 |
77 | export function move(arr, fromIndex, toIndex) {
78 | const element = arr.splice(fromIndex, 1)[0]
79 | arr.splice(toIndex, 0, element)
80 | return arr
81 | }
82 |
83 | export class UIDGenerator {
84 |
85 | constructor(seed) {
86 | this.seed = 'tfid-' + seed + '-'
87 | this.counter = 0
88 | }
89 |
90 | next() {
91 | return this.seed + (this.counter++)
92 | }
93 |
94 | }
95 |
96 | function containsUnion(type) {
97 | switch (type.meta.kind) {
98 | case 'union' :
99 | return true
100 | case 'maybe' :
101 | case 'subtype' :
102 | return containsUnion(type.meta.type)
103 | default :
104 | return false
105 | }
106 | }
107 |
108 | function getUnionConcreteType(type, value) {
109 | const kind = type.meta.kind
110 | if (kind === 'union') {
111 | const concreteType = type.dispatch(value)
112 | if (process.env.NODE_ENV !== 'production') {
113 | t.assert(t.isType(concreteType), () => 'Invalid value ' + t.assert.stringify(value) + ' supplied to ' + t.getTypeName(type) + ' (no constructor returned by dispatch)' )
114 | }
115 | return concreteType
116 | } else if (kind === 'maybe') {
117 | const maybeConcrete = t.maybe(getUnionConcreteType(type.meta.type, value), type.meta.name)
118 | maybeConcrete.getValidationErrorMessage = type.getValidationErrorMessage
119 | maybeConcrete.getTcombFormFactory = type.getTcombFormFactory
120 | return maybeConcrete
121 | } else if (kind === 'subtype') {
122 | const subtypeConcrete = t.subtype(getUnionConcreteType(type.meta.type, value), type.meta.predicate, type.meta.name)
123 | subtypeConcrete.getValidationErrorMessage = type.getValidationErrorMessage
124 | subtypeConcrete.getTcombFormFactory = type.getTcombFormFactory
125 | return subtypeConcrete
126 | }
127 | }
128 |
129 | export function getTypeFromUnion(type, value) {
130 | if (containsUnion(type)) {
131 | return getUnionConcreteType(type, value)
132 | }
133 | return type
134 | }
135 |
136 | function getUnion(type) {
137 | if (type.meta.kind === 'union') {
138 | return type
139 | }
140 | return getUnion(type.meta.type)
141 | }
142 |
143 | function findIndex(arr, element) {
144 | for (let i = 0, len = arr.length; i < len; i++ ) {
145 | if (arr[i] === element) {
146 | return i
147 | }
148 | }
149 | return -1
150 | }
151 |
152 | export function getBaseComponentOptions(options, defaultOptions, value, type) {
153 | if (t.Nil.is(options)) {
154 | return defaultOptions
155 | }
156 | if (t.Function.is(options)) {
157 | return options(value)
158 | }
159 | if (t.Array.is(options) && containsUnion(type)) {
160 | const union = getUnion(type)
161 | const concreteType = union.dispatch(value)
162 | const index = findIndex(union.meta.types, concreteType)
163 | // recurse
164 | return getComponentOptions(options[index], defaultOptions, value, concreteType) // eslint-disable-line no-use-before-define
165 | }
166 | return options
167 | }
168 |
169 | export function getComponentOptions(options, defaultOptions, value, type) {
170 | const opts = getBaseComponentOptions(options, defaultOptions, value, type)
171 | if (t.Function.is(type.getTcombFormOptions)) {
172 | return type.getTcombFormOptions(opts)
173 | }
174 | return opts
175 | }
176 |
177 | export function isArraysShallowDiffers(array, other) {
178 | if (array === other) {
179 | return false
180 | }
181 |
182 | const { length } = array
183 | if (length !== other.length) {
184 | return true
185 | }
186 |
187 | let index = -1
188 | while (++index < length) {
189 | if (array[index] !== other[index]) {
190 | return true
191 | }
192 | }
193 |
194 | return false
195 | }
196 |
--------------------------------------------------------------------------------
/test/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import t from 'tcomb-validation'
3 | import bootstrap from 'tcomb-form-templates-bootstrap'
4 | import React from 'react'
5 | import { Checkbox } from '../../src/components'
6 | import { ctx, ctxPlaceholders, getRenderComponent } from './util'
7 | const renderComponent = getRenderComponent(Checkbox)
8 |
9 | const transformer = {
10 | format: (value) => {
11 | if (t.String.is(value)) {
12 | return value
13 | } else if (value === true) {
14 | return '1'
15 | }
16 | return '0'
17 | },
18 | parse: (value) => value === '1'
19 | }
20 |
21 | tape('Checkbox', ({ test }) => {
22 | test('label', (assert) => {
23 | assert.plan(5)
24 |
25 | assert.strictEqual(
26 | new Checkbox({
27 | type: t.Bool,
28 | options: {},
29 | ctx: ctx
30 | }).getLocals().label,
31 | 'Default label',
32 | 'should have a default label')
33 |
34 | assert.strictEqual(
35 | new Checkbox({
36 | type: t.Bool,
37 | options: {},
38 | ctx: ctxPlaceholders
39 | }).getLocals().label,
40 | 'Default label',
41 | 'should have a default label even if auto !== labels')
42 |
43 | assert.strictEqual(
44 | new Checkbox({
45 | type: t.Bool,
46 | options: {label: 'mylabel'},
47 | ctx: ctx
48 | }).getLocals().label,
49 | 'mylabel',
50 | 'should handle label option as string')
51 |
52 | const actual = new Checkbox({
53 | type: t.Bool,
54 | options: {label: React.DOM.i(null, 'JSX label')},
55 | ctx: ctx
56 | }).getLocals().label
57 | assert.equal(actual.type, 'i')
58 | assert.equal(actual.props.children, 'JSX label')
59 | })
60 |
61 | test('help', (assert) => {
62 | assert.plan(3)
63 |
64 | assert.strictEqual(
65 | new Checkbox({
66 | type: t.Bool,
67 | options: {help: 'myhelp'},
68 | ctx: ctx
69 | }).getLocals().help,
70 | 'myhelp',
71 | 'should handle help option as string')
72 |
73 | const actual = new Checkbox({
74 | type: t.Bool,
75 | options: {help: React.DOM.i(null, 'JSX help')},
76 | ctx: ctx
77 | }).getLocals().help
78 | assert.equal(actual.type, 'i')
79 | assert.equal(actual.props.children, 'JSX help')
80 | })
81 |
82 | test('value', (assert) => {
83 | assert.plan(3)
84 |
85 | assert.strictEqual(
86 | new Checkbox({
87 | type: t.Bool,
88 | options: {},
89 | ctx: ctx
90 | }).getLocals().value,
91 | false,
92 | 'default value should be false')
93 |
94 | assert.strictEqual(
95 | new Checkbox({
96 | type: t.Bool,
97 | options: {},
98 | ctx: ctx,
99 | value: false
100 | }).getLocals().value,
101 | false,
102 | 'should handle value option')
103 |
104 | assert.strictEqual(
105 | new Checkbox({
106 | type: t.Bool,
107 | options: {},
108 | ctx: ctx,
109 | value: true
110 | }).getLocals().value,
111 | true,
112 | 'should handle value option')
113 | })
114 |
115 | test('transformer', (assert) => {
116 | assert.plan(1)
117 |
118 | assert.strictEqual(
119 | new Checkbox({
120 | type: t.Bool,
121 | options: {transformer: transformer},
122 | ctx: ctx,
123 | value: true
124 | }).getLocals().value,
125 | '1',
126 | 'should handle transformer option (format)')
127 | })
128 |
129 | test('hasError', (assert) => {
130 | assert.plan(2)
131 |
132 | const True = t.subtype(t.Bool, (value) => value === true)
133 |
134 | assert.strictEqual(
135 | new Checkbox({
136 | type: True,
137 | options: {},
138 | ctx: ctx
139 | }).getLocals().hasError,
140 | false,
141 | 'default hasError should be false')
142 |
143 | assert.strictEqual(
144 | new Checkbox({
145 | type: True,
146 | options: {hasError: true},
147 | ctx: ctx
148 | }).getLocals().hasError,
149 | true,
150 | 'should handle hasError option')
151 | })
152 |
153 | test('error', (assert) => {
154 | assert.plan(3)
155 |
156 | assert.strictEqual(
157 | new Checkbox({
158 | type: t.Bool,
159 | options: {},
160 | ctx: ctx
161 | }).getLocals().error,
162 | undefined,
163 | 'default error should be undefined')
164 |
165 | assert.strictEqual(
166 | new Checkbox({
167 | type: t.Bool,
168 | options: {error: 'myerror', hasError: true},
169 | ctx: ctx
170 | }).getLocals().error,
171 | 'myerror',
172 | 'should handle error option')
173 |
174 | assert.strictEqual(
175 | new Checkbox({
176 | type: t.Bool,
177 | options: {
178 | error: (value) => 'error: ' + value,
179 | hasError: true
180 | },
181 | ctx: ctx,
182 | value: 'a'
183 | }).getLocals().error,
184 | 'error: a',
185 | 'should handle error option as a function')
186 | })
187 |
188 | test('template', (assert) => {
189 | assert.plan(2)
190 |
191 | assert.strictEqual(
192 | new Checkbox({
193 | type: t.Bool,
194 | options: {},
195 | ctx: ctx
196 | }).getTemplate(),
197 | bootstrap.checkbox,
198 | 'default template should be bootstrap.checkbox')
199 |
200 | const template = () => {}
201 |
202 | assert.strictEqual(
203 | new Checkbox({
204 | type: t.Bool,
205 | options: {template: template},
206 | ctx: ctx
207 | }).getTemplate(),
208 | template,
209 | 'should handle template option')
210 | })
211 |
212 | if (typeof window !== 'undefined') {
213 | test('validate', (assert) => {
214 | assert.plan(6)
215 |
216 | let result
217 |
218 | // required type, default value
219 | result = renderComponent({
220 | type: t.Bool
221 | }).validate()
222 |
223 | assert.strictEqual(result.isValid(), true)
224 | assert.strictEqual(result.value, false)
225 |
226 | // required type, setting a value
227 | result = renderComponent({
228 | type: t.Bool,
229 | value: true
230 | }).validate()
231 |
232 | assert.strictEqual(result.isValid(), true)
233 | assert.strictEqual(result.value, true)
234 |
235 | result = renderComponent({
236 | type: t.Bool,
237 | options: {transformer: transformer},
238 | value: true
239 | }).validate()
240 |
241 | // 'should handle transformer option (parse)'
242 | assert.strictEqual(result.isValid(), true)
243 | assert.strictEqual(result.value, true)
244 | })
245 | }
246 | })
247 |
--------------------------------------------------------------------------------
/test/components/Component.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import React from 'react'
3 | import t from 'tcomb-validation'
4 | import ShallowRenderer from 'react-test-renderer/shallow'
5 | import { Component, getComponent } from '../../src/components'
6 | import { ctx } from './util'
7 |
8 | tape('Component', ({ test }) => {
9 | test('getComponent public API', (assert) => {
10 | assert.plan(1)
11 | assert.ok(t.Function.is(getComponent))
12 | })
13 |
14 | test('getValidationErrorMessage', (assert) => {
15 | assert.plan(16)
16 |
17 | let component = new Component({
18 | type: t.String,
19 | options: {hasError: true},
20 | ctx: ctx
21 | })
22 |
23 | assert.strictEqual(component.getError(), undefined, '#1')
24 |
25 | component = new Component({
26 | type: t.maybe(t.String),
27 | options: {hasError: true},
28 | ctx: ctx
29 | })
30 |
31 | assert.strictEqual(component.getError(), undefined, '#2')
32 |
33 | const Password = t.refinement(t.String, (s) => s.length > 6)
34 |
35 | component = new Component({
36 | type: Password,
37 | options: {hasError: true},
38 | ctx: ctx
39 | })
40 |
41 | assert.strictEqual(component.getError(), undefined, '#3')
42 |
43 | Password.getValidationErrorMessage = () => 'bad password'
44 |
45 | component = new Component({
46 | type: Password,
47 | options: {hasError: false},
48 | ctx: ctx
49 | })
50 |
51 | assert.strictEqual(component.getError(), undefined, '#4.1')
52 |
53 | component = new Component({
54 | type: Password,
55 | options: {hasError: true},
56 | ctx: ctx
57 | })
58 |
59 | assert.strictEqual(component.getError(), 'bad password', '#4.2')
60 |
61 | component = new Component({
62 | type: t.maybe(Password),
63 | options: {hasError: true},
64 | ctx: ctx
65 | })
66 |
67 | assert.strictEqual(component.getError(), undefined, '#5')
68 |
69 | component = new Component({
70 | type: t.maybe(Password),
71 | options: {hasError: true},
72 | ctx: ctx,
73 | value: 'a'
74 | })
75 |
76 | assert.strictEqual(component.getError(), 'bad password', '#6')
77 |
78 | t.String.getValidationErrorMessage = () => 'bad string'
79 |
80 | component = new Component({
81 | type: Password,
82 | options: {hasError: true},
83 | ctx: ctx
84 | })
85 |
86 | assert.strictEqual(component.getError(), 'bad string', '#7')
87 |
88 | delete t.String.getValidationErrorMessage
89 |
90 | const Age = t.refinement(t.Number, (n) => n > 6)
91 |
92 | component = new Component({
93 | type: Age,
94 | options: {hasError: true},
95 | ctx: ctx
96 | })
97 |
98 | assert.strictEqual(component.getError(), undefined, '#8')
99 |
100 | component = new Component({
101 | type: t.maybe(Age),
102 | options: {hasError: true},
103 | ctx: ctx
104 | })
105 |
106 | assert.strictEqual(component.getError(), undefined, '#9')
107 |
108 | Age.getValidationErrorMessage = () => 'bad age'
109 |
110 | component = new Component({
111 | type: Age,
112 | options: {hasError: true},
113 | ctx: ctx
114 | })
115 |
116 | assert.strictEqual(component.getError(), 'bad age', '#10')
117 |
118 | component = new Component({
119 | type: t.maybe(Age),
120 | options: {hasError: true},
121 | ctx: ctx
122 | })
123 |
124 | assert.strictEqual(component.getError(), undefined, '#11')
125 |
126 | component = new Component({
127 | type: t.maybe(Age),
128 | options: {hasError: true},
129 | ctx: ctx,
130 | value: 'a'
131 | })
132 |
133 | assert.strictEqual(component.getError(), 'bad age', '#12')
134 |
135 | t.Number.getValidationErrorMessage = () => 'bad number'
136 |
137 | component = new Component({
138 | type: Age,
139 | options: {hasError: true},
140 | ctx: ctx,
141 | value: 'a'
142 | })
143 |
144 | assert.strictEqual(component.getError(), 'bad number', '#13')
145 |
146 | component = new Component({
147 | type: Age,
148 | options: {hasError: true},
149 | ctx: ctx,
150 | value: 1
151 | })
152 |
153 | assert.strictEqual(component.getError(), 'bad age', '#14')
154 |
155 | component = new Component({
156 | type: t.maybe(Age),
157 | options: {hasError: true},
158 | ctx: ctx
159 | })
160 |
161 | assert.strictEqual(component.getError(), undefined, '#15')
162 |
163 | delete t.Number.getValidationErrorMessage
164 | })
165 |
166 | test('typeInfo', (assert) => {
167 | assert.plan(3)
168 |
169 | const MaybeString = t.maybe(t.String)
170 | const Subtype = t.subtype(t.String, () => true )
171 |
172 | let component = new Component({
173 | type: t.String,
174 | options: {},
175 | ctx: ctx
176 | })
177 |
178 | assert.deepEqual(component.typeInfo, {
179 | type: t.String,
180 | isMaybe: false,
181 | isSubtype: false,
182 | innerType: t.Str,
183 | getValidationErrorMessage: undefined
184 | })
185 |
186 | component = new Component({
187 | type: MaybeString,
188 | options: {},
189 | ctx: ctx
190 | })
191 |
192 | assert.deepEqual(component.typeInfo, {
193 | type: MaybeString,
194 | isMaybe: true,
195 | isSubtype: false,
196 | innerType: t.Str,
197 | getValidationErrorMessage: undefined
198 | })
199 |
200 | component = new Component({
201 | type: Subtype,
202 | options: {},
203 | ctx: ctx
204 | })
205 |
206 | assert.deepEqual(component.typeInfo, {
207 | type: Subtype,
208 | isMaybe: false,
209 | isSubtype: true,
210 | innerType: t.Str,
211 | getValidationErrorMessage: undefined
212 | })
213 | })
214 |
215 | test('getId()', (assert) => {
216 | assert.plan(1)
217 |
218 | assert.strictEqual(
219 | new Component({
220 | type: t.Str,
221 | options: {attrs: {id: 'myid'}},
222 | ctx: ctx
223 | }).getId(),
224 | 'myid',
225 | 'should return the provided id')
226 | })
227 |
228 | test('re-render on path change', (assert) => {
229 | assert.plan(2)
230 |
231 | let renderCallsNum = 0
232 |
233 | class TestComponent extends Component {
234 | render() {
235 | renderCallsNum++
236 | return null
237 | }
238 | }
239 |
240 | const renderer = new ShallowRenderer()
241 |
242 | const baseProps = {
243 | options: {},
244 | }
245 | const elementA = React.createElement(TestComponent, {
246 | ...baseProps,
247 | ctx: {
248 | ...ctx,
249 | path: ['foo']
250 | }
251 | })
252 | const elementB = React.createElement(TestComponent, {
253 | ...baseProps,
254 | ctx: {
255 | ...ctx,
256 | path: ['bar']
257 | }
258 | })
259 |
260 | renderer.render(elementA)
261 | assert.strictEqual(renderCallsNum, 1, '#1.1')
262 |
263 | renderer.render(elementB)
264 | assert.strictEqual(renderCallsNum, 2, '#1.2')
265 | })
266 | })
267 |
--------------------------------------------------------------------------------
/test/components/Datetime.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import t from 'tcomb-validation'
3 | import bootstrap from 'tcomb-form-templates-bootstrap'
4 | import React from 'react'
5 | import { Datetime } from '../../src/components'
6 | import { ctx, getRenderComponent } from './util'
7 | const renderComponent = getRenderComponent(Datetime)
8 |
9 | tape('Datetime', ({ test }) => {
10 | test('label', (assert) => {
11 | assert.plan(4)
12 |
13 | assert.strictEqual(
14 | new Datetime({
15 | type: t.Dat,
16 | options: {},
17 | ctx: ctx
18 | }).getLocals().label,
19 | 'Default label',
20 | 'should have a default label')
21 |
22 | assert.strictEqual(
23 | new Datetime({
24 | type: t.Dat,
25 | options: {label: 'mylabel'},
26 | ctx: ctx
27 | }).getLocals().label,
28 | 'mylabel',
29 | 'should handle label option as string')
30 |
31 | const actual = new Datetime({
32 | type: t.Dat,
33 | options: {label: React.DOM.i(null, 'JSX label')},
34 | ctx: ctx
35 | }).getLocals().label
36 | assert.equal(actual.type, 'i')
37 | assert.equal(actual.props.children, 'JSX label')
38 | })
39 |
40 | test('help', (assert) => {
41 | assert.plan(3)
42 |
43 | assert.strictEqual(
44 | new Datetime({
45 | type: t.Dat,
46 | options: {help: 'myhelp'},
47 | ctx: ctx
48 | }).getLocals().help,
49 | 'myhelp',
50 | 'should handle help option as string')
51 |
52 | const actual = new Datetime({
53 | type: t.Dat,
54 | options: {help: React.DOM.i(null, 'JSX help')},
55 | ctx: ctx
56 | }).getLocals().help
57 | assert.equal(actual.type, 'i')
58 | assert.equal(actual.props.children, 'JSX help')
59 | })
60 |
61 | test('value', (assert) => {
62 | assert.plan(2)
63 |
64 | assert.deepEqual(
65 | new Datetime({
66 | type: t.Dat,
67 | options: {},
68 | ctx: ctx
69 | }).getLocals().value,
70 | [null, null, null],
71 | 'default value should be [null, null, null]')
72 |
73 | assert.deepEqual(
74 | new Datetime({
75 | type: t.Dat,
76 | options: {},
77 | ctx: ctx,
78 | value: new Date(1973, 10, 30)
79 | }).getLocals().value,
80 | ['1973', '10', '30'],
81 | 'should handle value option')
82 | })
83 |
84 | test('hasError', (assert) => {
85 | assert.plan(2)
86 |
87 | assert.strictEqual(
88 | new Datetime({
89 | type: t.Dat,
90 | options: {},
91 | ctx: ctx
92 | }).getLocals().hasError,
93 | false,
94 | 'default hasError should be false')
95 |
96 | assert.strictEqual(
97 | new Datetime({
98 | type: t.Dat,
99 | options: {hasError: true},
100 | ctx: ctx
101 | }).getLocals().hasError,
102 | true,
103 | 'should handle hasError option')
104 | })
105 |
106 | test('error', (assert) => {
107 | assert.plan(3)
108 |
109 | assert.strictEqual(
110 | new Datetime({
111 | type: t.Dat,
112 | options: {},
113 | ctx: ctx
114 | }).getLocals().error,
115 | undefined,
116 | 'default error should be undefined')
117 |
118 | assert.strictEqual(
119 | new Datetime({
120 | type: t.Dat,
121 | options: {error: 'myerror', hasError: true},
122 | ctx: ctx
123 | }).getLocals().error,
124 | 'myerror',
125 | 'should handle error option')
126 |
127 | assert.deepEqual(
128 | new Datetime({
129 | type: t.Dat,
130 | options: {
131 | error: (value) => 'error: ' + value.getFullYear(),
132 | hasError: true
133 | },
134 | ctx: ctx,
135 | value: new Date(1973, 10, 30)
136 | }).getLocals().error,
137 | 'error: 1973',
138 | 'should handle error option as a function')
139 | })
140 |
141 | test('template', (assert) => {
142 | assert.plan(2)
143 |
144 | assert.strictEqual(
145 | new Datetime({
146 | type: t.Dat,
147 | options: {},
148 | ctx: ctx
149 | }).getTemplate(),
150 | bootstrap.date,
151 | 'default template should be bootstrap.date')
152 |
153 | const template = () => {}
154 |
155 | assert.strictEqual(
156 | new Datetime({
157 | type: t.Dat,
158 | options: {template: template},
159 | ctx: ctx
160 | }).getTemplate(),
161 | template,
162 | 'should handle template option')
163 | })
164 |
165 | if (typeof window !== 'undefined') {
166 | test('validate', (assert) => {
167 | assert.plan(4)
168 |
169 | let result
170 |
171 | // required type, default value
172 | result = renderComponent({
173 | type: t.Dat
174 | }).validate()
175 |
176 | assert.strictEqual(result.isValid(), false)
177 | assert.deepEqual(result.value, null)
178 |
179 | // required type, setting a value
180 | result = renderComponent({
181 | type: t.Dat,
182 | value: new Date(1973, 10, 30)
183 | }).validate()
184 |
185 | assert.strictEqual(result.isValid(), true)
186 | assert.strictEqual(result.value.getFullYear(), 1973)
187 | })
188 | }
189 | })
190 |
--------------------------------------------------------------------------------
/test/components/List.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import t from 'tcomb-validation'
3 | import { List } from '../../src/components'
4 | import { ctx } from './util'
5 |
6 | tape('List', ({ test }) => {
7 | test('should support unions', (assert) => {
8 | assert.plan(2)
9 |
10 | const AccountType = t.enums.of([
11 | 'type 1',
12 | 'type 2',
13 | 'other'
14 | ], 'AccountType')
15 |
16 | const KnownAccount = t.struct({
17 | type: AccountType
18 | }, 'KnownAccount')
19 |
20 | const UnknownAccount = KnownAccount.extend({
21 | label: t.String,
22 | }, 'UnknownAccount')
23 |
24 | const Account = t.union([KnownAccount, UnknownAccount], 'Account')
25 |
26 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount
27 |
28 | let component = new List({
29 | type: t.list(Account),
30 | ctx: ctx,
31 | options: {},
32 | value: [
33 | { type: 'type 1' }
34 | ]
35 | })
36 |
37 | assert.strictEqual(component.getItems()[0].input.props.type, KnownAccount)
38 |
39 | component = new List({
40 | type: t.list(Account),
41 | ctx: ctx,
42 | options: {},
43 | value: [
44 | { type: 'other' }
45 | ]
46 | })
47 |
48 | assert.strictEqual(component.getItems()[0].input.props.type, UnknownAccount)
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/test/components/Radio.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import t from 'tcomb-validation'
3 | import bootstrap from 'tcomb-form-templates-bootstrap'
4 | import React from 'react'
5 | import { Radio } from '../../src/components'
6 | import { ctx, getRenderComponent } from './util'
7 | const renderComponent = getRenderComponent(Radio)
8 |
9 | const transformer = {
10 | format: (value) => {
11 | if (t.String.is(value)) {
12 | return value
13 | } else if (value === true) {
14 | return '1'
15 | }
16 | return '0'
17 | },
18 | parse: (value) => value === '1'
19 | }
20 |
21 | tape('Radio', ({ test }) => {
22 | const Country = t.enums({
23 | 'IT': 'Italy',
24 | 'FR': 'France',
25 | 'US': 'United States'
26 | })
27 |
28 | test('label', (assert) => {
29 | assert.plan(5)
30 |
31 | assert.strictEqual(
32 | new Radio({
33 | type: Country,
34 | options: {},
35 | ctx: ctx
36 | }).getLocals().label,
37 | 'Default label',
38 | 'should have a default label')
39 |
40 | assert.strictEqual(
41 | new Radio({
42 | type: Country,
43 | options: {label: 'mylabel'},
44 | ctx: ctx
45 | }).getLocals().label,
46 | 'mylabel',
47 | 'should handle label option as string')
48 |
49 | const actual = new Radio({
50 | type: Country,
51 | options: {label: React.DOM.i(null, 'JSX label')},
52 | ctx: ctx
53 | }).getLocals().label
54 | assert.equal(actual.type, 'i')
55 | assert.equal(actual.props.children, 'JSX label')
56 |
57 | assert.strictEqual(
58 | new Radio({
59 | type: t.maybe(Country),
60 | options: {},
61 | ctx: ctx
62 | }).getLocals().label,
63 | 'Default label (optional)',
64 | 'should handle optional types')
65 | })
66 |
67 | test('help', (assert) => {
68 | assert.plan(3)
69 |
70 | assert.strictEqual(
71 | new Radio({
72 | type: Country,
73 | options: {help: 'myhelp'},
74 | ctx: ctx
75 | }).getLocals().help,
76 | 'myhelp',
77 | 'should handle help option as string')
78 |
79 | const actual = new Radio({
80 | type: Country,
81 | options: {help: React.DOM.i(null, 'JSX help')},
82 | ctx: ctx
83 | }).getLocals().help
84 | assert.equal(actual.type, 'i')
85 | assert.equal(actual.props.children, 'JSX help')
86 | })
87 |
88 | test('value', (assert) => {
89 | assert.plan(1)
90 |
91 | assert.strictEqual(
92 | new Radio({
93 | type: Country,
94 | options: {},
95 | ctx: ctx,
96 | value: 'a'
97 | }).getLocals().value,
98 | 'a',
99 | 'should handle value option')
100 | })
101 |
102 | test('transformer', (assert) => {
103 | assert.plan(1)
104 |
105 | assert.strictEqual(
106 | new Radio({
107 | type: t.maybe(t.Bool),
108 | options: {
109 | transformer: transformer,
110 | options: [
111 | {value: '0', text: 'No'},
112 | {value: '1', text: 'Yes'}
113 | ]
114 | },
115 | ctx: ctx,
116 | value: true
117 | }).getLocals().value,
118 | '1',
119 | 'should handle transformer option (format)')
120 | })
121 |
122 | test('hasError', (assert) => {
123 | assert.plan(2)
124 |
125 | assert.strictEqual(
126 | new Radio({
127 | type: Country,
128 | options: {},
129 | ctx: ctx
130 | }).getLocals().hasError,
131 | false,
132 | 'default hasError should be false')
133 |
134 | assert.strictEqual(
135 | new Radio({
136 | type: Country,
137 | options: {hasError: true},
138 | ctx: ctx
139 | }).getLocals().hasError,
140 | true,
141 | 'should handle hasError option')
142 | })
143 |
144 | test('error', (assert) => {
145 | assert.plan(3)
146 |
147 | assert.strictEqual(
148 | new Radio({
149 | type: Country,
150 | options: {},
151 | ctx: ctx
152 | }).getLocals().error,
153 | undefined,
154 | 'default error should be undefined')
155 |
156 | assert.strictEqual(
157 | new Radio({
158 | type: Country,
159 | options: {error: 'myerror', hasError: true},
160 | ctx: ctx
161 | }).getLocals().error,
162 | 'myerror',
163 | 'should handle error option')
164 |
165 | assert.strictEqual(
166 | new Radio({
167 | type: Country,
168 | options: {
169 | error: (value) => 'error: ' + value,
170 | hasError: true
171 | },
172 | ctx: ctx,
173 | value: 'a'
174 | }).getLocals().error,
175 | 'error: a',
176 | 'should handle error option as a function')
177 | })
178 |
179 | test('template', (assert) => {
180 | assert.plan(2)
181 |
182 | assert.strictEqual(
183 | new Radio({
184 | type: Country,
185 | options: {},
186 | ctx: ctx
187 | }).getTemplate(),
188 | bootstrap.radio,
189 | 'default template should be bootstrap.eadio')
190 |
191 | const template = () => {}
192 |
193 | assert.strictEqual(
194 | new Radio({
195 | type: Country,
196 | options: {template: template},
197 | ctx: ctx
198 | }).getTemplate(),
199 | template,
200 | 'should handle template option')
201 | })
202 |
203 | test('options', (assert) => {
204 | assert.plan(1)
205 |
206 | assert.deepEqual(
207 | new Radio({
208 | type: Country,
209 | options: {
210 | options: [
211 | {value: 'IT', text: 'Italia'},
212 | {value: 'US', text: 'Stati Uniti'}
213 | ]
214 | },
215 | ctx: ctx
216 | }).getLocals().options,
217 | [
218 | { text: 'Italia', value: 'IT' },
219 | { text: 'Stati Uniti', value: 'US' }
220 | ],
221 | 'should handle options option')
222 | })
223 |
224 | test('order', (assert) => {
225 | assert.plan(2)
226 |
227 | assert.deepEqual(
228 | new Radio({
229 | type: Country,
230 | options: {order: 'asc'},
231 | ctx: ctx
232 | }).getLocals().options,
233 | [
234 | { text: 'France', value: 'FR' },
235 | { text: 'Italy', value: 'IT' },
236 | { text: 'United States', value: 'US' }
237 | ],
238 | 'should handle order = asc option')
239 |
240 | assert.deepEqual(
241 | new Radio({
242 | type: Country,
243 | options: {order: 'desc'},
244 | ctx: ctx
245 | }).getLocals().options,
246 | [
247 | { text: 'United States', value: 'US' },
248 | { text: 'Italy', value: 'IT' },
249 | { text: 'France', value: 'FR' }
250 | ],
251 | 'should handle order = desc option')
252 | })
253 |
254 | if (typeof window !== 'undefined') {
255 | test('validate', (assert) => {
256 | assert.plan(8)
257 |
258 | let result
259 |
260 | // required type, default value
261 | result = renderComponent({
262 | type: Country
263 | }).validate()
264 |
265 | assert.strictEqual(result.isValid(), false)
266 | assert.strictEqual(result.value, null)
267 |
268 | // required type, setting a value
269 | result = renderComponent({
270 | type: Country,
271 | value: 'IT'
272 | }).validate()
273 |
274 | assert.strictEqual(result.isValid(), true)
275 | assert.strictEqual(result.value, 'IT')
276 |
277 | // optional type
278 | result = renderComponent({
279 | type: t.maybe(Country)
280 | }).validate()
281 |
282 | assert.strictEqual(result.isValid(), true)
283 | assert.strictEqual(result.value, null)
284 |
285 | result = renderComponent({
286 | type: t.maybe(t.Bool),
287 | options: {
288 | transformer: transformer,
289 | options: [
290 | {value: '0', text: 'No'},
291 | {value: '1', text: 'Yes'}
292 | ]
293 | },
294 | value: true
295 | }).validate()
296 |
297 | assert.strictEqual(result.isValid(), true)
298 | assert.strictEqual(result.value, true)
299 | })
300 | }
301 | })
302 |
--------------------------------------------------------------------------------
/test/components/Select.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import t from 'tcomb-validation'
3 | import bootstrap from 'tcomb-form-templates-bootstrap'
4 | import React from 'react'
5 | import { Select } from '../../src/components'
6 | import { ctx, getRenderComponent } from './util'
7 | const renderComponent = getRenderComponent(Select)
8 |
9 | const transformer = {
10 | format: (value) => {
11 | if (t.String.is(value)) {
12 | return value
13 | } else if (value === true) {
14 | return '1'
15 | }
16 | return '0'
17 | },
18 | parse: (value) => value === '1'
19 | }
20 |
21 | tape('Select', ({ test }) => {
22 | const Country = t.enums({
23 | 'IT': 'Italy',
24 | 'FR': 'France',
25 | 'US': 'United States'
26 | })
27 |
28 | test('label', (assert) => {
29 | assert.plan(5)
30 |
31 | assert.strictEqual(
32 | new Select({
33 | type: Country,
34 | options: {},
35 | ctx: ctx
36 | }).getLocals().label,
37 | 'Default label',
38 | 'should have a default label')
39 |
40 | assert.strictEqual(
41 | new Select({
42 | type: Country,
43 | options: {label: 'mylabel'},
44 | ctx: ctx
45 | }).getLocals().label,
46 | 'mylabel',
47 | 'should handle label option as string')
48 |
49 | const actual = new Select({
50 | type: Country,
51 | options: {label: React.DOM.i(null, 'JSX label')},
52 | ctx: ctx
53 | }).getLocals().label
54 | assert.equal(actual.type, 'i')
55 | assert.equal(actual.props.children, 'JSX label')
56 |
57 | assert.strictEqual(
58 | new Select({
59 | type: t.maybe(Country),
60 | options: {},
61 | ctx: ctx
62 | }).getLocals().label,
63 | 'Default label (optional)',
64 | 'should handle optional types')
65 | })
66 |
67 | test('help', (assert) => {
68 | assert.plan(3)
69 |
70 | assert.strictEqual(
71 | new Select({
72 | type: Country,
73 | options: {help: 'myhelp'},
74 | ctx: ctx
75 | }).getLocals().help,
76 | 'myhelp',
77 | 'should handle help option as string')
78 |
79 | const actual = new Select({
80 | type: Country,
81 | options: {help: React.DOM.i(null, 'JSX help')},
82 | ctx: ctx
83 | }).getLocals().help
84 | assert.equal(actual.type, 'i')
85 | assert.equal(actual.props.children, 'JSX help')
86 | })
87 |
88 | test('value', (assert) => {
89 | assert.plan(2)
90 |
91 | assert.strictEqual(
92 | new Select({
93 | type: Country,
94 | options: {},
95 | ctx: ctx
96 | }).getLocals().value,
97 | '',
98 | 'default value should be nullOption.value')
99 |
100 | assert.strictEqual(
101 | new Select({
102 | type: Country,
103 | options: {},
104 | ctx: ctx,
105 | value: 'a'
106 | }).getLocals().value,
107 | 'a',
108 | 'should handle value option')
109 | })
110 |
111 | test('transformer', (assert) => {
112 | assert.plan(1)
113 |
114 | assert.strictEqual(
115 | new Select({
116 | type: t.maybe(t.Bool),
117 | options: {
118 | transformer: transformer,
119 | options: [
120 | {value: '0', text: 'No'},
121 | {value: '1', text: 'Yes'}
122 | ]
123 | },
124 | ctx: ctx,
125 | value: true
126 | }).getLocals().value,
127 | '1',
128 | 'should handle transformer option (format)')
129 | })
130 |
131 | test('hasError', (assert) => {
132 | assert.plan(2)
133 |
134 | assert.strictEqual(
135 | new Select({
136 | type: Country,
137 | options: {},
138 | ctx: ctx
139 | }).getLocals().hasError,
140 | false,
141 | 'default hasError should be false')
142 |
143 | assert.strictEqual(
144 | new Select({
145 | type: Country,
146 | options: {hasError: true},
147 | ctx: ctx
148 | }).getLocals().hasError,
149 | true,
150 | 'should handle hasError option')
151 | })
152 |
153 | test('error', (assert) => {
154 | assert.plan(3)
155 |
156 | assert.strictEqual(
157 | new Select({
158 | type: Country,
159 | options: {},
160 | ctx: ctx
161 | }).getLocals().error,
162 | undefined,
163 | 'default error should be undefined')
164 |
165 | assert.strictEqual(
166 | new Select({
167 | type: Country,
168 | options: {error: 'myerror', hasError: true},
169 | ctx: ctx
170 | }).getLocals().error,
171 | 'myerror',
172 | 'should handle error option')
173 |
174 | assert.strictEqual(
175 | new Select({
176 | type: Country,
177 | options: {
178 | error: (value) => 'error: ' + value,
179 | hasError: true
180 | },
181 | ctx: ctx,
182 | value: 'a'
183 | }).getLocals().error,
184 | 'error: a',
185 | 'should handle error option as a function')
186 | })
187 |
188 | test('template', (assert) => {
189 | assert.plan(2)
190 |
191 | assert.strictEqual(
192 | new Select({
193 | type: Country,
194 | options: {},
195 | ctx: ctx
196 | }).getTemplate(),
197 | bootstrap.select,
198 | 'default template should be bootstrap.select')
199 |
200 | const template = () => {}
201 |
202 | assert.strictEqual(
203 | new Select({
204 | type: Country,
205 | options: {template: template},
206 | ctx: ctx
207 | }).getTemplate(),
208 | template,
209 | 'should handle template option')
210 | })
211 |
212 | test('options', (assert) => {
213 | assert.plan(1)
214 |
215 | assert.deepEqual(
216 | new Select({
217 | type: Country,
218 | options: {
219 | options: [
220 | {value: 'IT', text: 'Italia'},
221 | {value: 'US', text: 'Stati Uniti'}
222 | ]
223 | },
224 | ctx: ctx
225 | }).getLocals().options,
226 | [
227 | { text: '-', value: '' },
228 | { text: 'Italia', value: 'IT' },
229 | { text: 'Stati Uniti', value: 'US' }
230 | ],
231 | 'should handle options option')
232 | })
233 |
234 | test('order', (assert) => {
235 | assert.plan(2)
236 |
237 | assert.deepEqual(
238 | new Select({
239 | type: Country,
240 | options: {order: 'asc'},
241 | ctx: ctx
242 | }).getLocals().options,
243 | [
244 | { text: '-', value: '' },
245 | { text: 'France', value: 'FR' },
246 | { text: 'Italy', value: 'IT' },
247 | { text: 'United States', value: 'US' }
248 | ],
249 | 'should handle order = asc option')
250 |
251 | assert.deepEqual(
252 | new Select({
253 | type: Country,
254 | options: {order: 'desc'},
255 | ctx: ctx
256 | }).getLocals().options,
257 | [
258 | { text: '-', value: '' },
259 | { text: 'United States', value: 'US' },
260 | { text: 'Italy', value: 'IT' },
261 | { text: 'France', value: 'FR' }
262 | ],
263 | 'should handle order = desc option')
264 | })
265 |
266 | test('nullOption', (assert) => {
267 | assert.plan(2)
268 |
269 | assert.deepEqual(
270 | new Select({
271 | type: Country,
272 | options: {
273 | nullOption: {value: '', text: 'Select a country'}
274 | },
275 | ctx: ctx
276 | }).getLocals().options,
277 | [
278 | { value: '', text: 'Select a country' },
279 | { text: 'Italy', value: 'IT' },
280 | { text: 'France', value: 'FR' },
281 | { text: 'United States', value: 'US' }
282 | ],
283 | 'should handle nullOption option')
284 |
285 | assert.deepEqual(
286 | new Select({
287 | type: Country,
288 | options: {
289 | nullOption: false
290 | },
291 | ctx: ctx,
292 | value: 'US'
293 | }).getLocals().options,
294 | [
295 | { text: 'Italy', value: 'IT' },
296 | { text: 'France', value: 'FR' },
297 | { text: 'United States', value: 'US' }
298 | ],
299 | 'should skip the nullOption if nullOption = false')
300 | })
301 |
302 | if (typeof window !== 'undefined') {
303 | test('validate', (assert) => {
304 | assert.plan(16)
305 |
306 | let result
307 |
308 | // required type, default value
309 | result = renderComponent({
310 | type: Country
311 | }).validate()
312 |
313 | assert.strictEqual(result.isValid(), false)
314 | assert.strictEqual(result.value, null)
315 |
316 | // required type, setting a value
317 | result = renderComponent({
318 | type: Country,
319 | value: 'IT'
320 | }).validate()
321 |
322 | assert.strictEqual(result.isValid(), true)
323 | assert.strictEqual(result.value, 'IT')
324 |
325 | // optional type
326 | result = renderComponent({
327 | type: t.maybe(Country)
328 | }).validate()
329 |
330 | assert.strictEqual(result.isValid(), true)
331 | assert.strictEqual(result.value, null)
332 |
333 | // option groups
334 | const Car = t.enums.of('Audi Chrysler Ford Renault Peugeot')
335 | result = renderComponent({
336 | type: Car,
337 | options: {
338 | options: [
339 | {value: 'Audi', text: 'Audi'}, // an option
340 | {label: 'US', options: [ // a group of options
341 | {value: 'Chrysler', text: 'Chrysler'},
342 | {value: 'Ford', text: 'Ford'}
343 | ]},
344 | {label: 'France', options: [ // another group of options
345 | {value: 'Renault', text: 'Renault'},
346 | {value: 'Peugeot', text: 'Peugeot'}
347 | ], disabled: true} // use `disabled: true` to disable an optgroup
348 | ]
349 | },
350 | value: 'Ford'
351 | }).validate()
352 |
353 | assert.strictEqual(result.isValid(), true)
354 | assert.strictEqual(result.value, 'Ford')
355 |
356 | //
357 | // multiple select
358 | //
359 |
360 | // default value should be []
361 | result = renderComponent({
362 | type: t.list(Country)
363 | }).validate()
364 |
365 | assert.strictEqual(result.isValid(), true)
366 | assert.deepEqual(result.value, [])
367 |
368 | // setting a value
369 | result = renderComponent({
370 | type: t.list(Country),
371 | value: ['IT', 'US']
372 | }).validate()
373 |
374 | assert.strictEqual(result.isValid(), true)
375 | assert.deepEqual(result.value, ['IT', 'US'])
376 |
377 | // subtyped multiple select
378 | result = renderComponent({
379 | type: t.subtype(t.list(Country), (x) => x.length >= 2),
380 | value: ['IT']
381 | }).validate()
382 |
383 | assert.strictEqual(result.isValid(), false)
384 | assert.deepEqual(result.value, ['IT'])
385 |
386 | // should handle transformer option (parse)
387 | result = renderComponent({
388 | type: t.maybe(t.Bool),
389 | options: {
390 | transformer: transformer,
391 | options: [
392 | {value: '0', text: 'No'},
393 | {value: '1', text: 'Yes'}
394 | ]
395 | },
396 | value: true
397 | }).validate()
398 |
399 | assert.strictEqual(result.isValid(), true)
400 | assert.deepEqual(result.value, true)
401 | })
402 | }
403 | })
404 |
--------------------------------------------------------------------------------
/test/components/Struct.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import React from 'react'
3 | import ReactTestRenderer from 'react-test-renderer'
4 | import t from '../../src/main'
5 | import { Struct, Form } from '../../src/components'
6 | import { ctx } from './util'
7 |
8 | tape('Struct', ({ test }) => {
9 | test('options can be a function', (assert) => {
10 | assert.plan(2)
11 |
12 | let component = new Struct({
13 | type: t.struct({
14 | name: t.String
15 | }),
16 | options: {
17 | fields: {
18 | name: value => ({ disabled: value === 'a' })
19 | }
20 | },
21 | ctx: ctx
22 | })
23 |
24 | assert.strictEqual(component.getInputs().name.props.options.disabled, false)
25 |
26 | component = new Struct({
27 | type: t.struct({
28 | name: t.String
29 | }),
30 | options: {
31 | fields: {
32 | name: value => ({ disabled: value === 'a' })
33 | }
34 | },
35 | ctx: ctx,
36 | value: { name: 'a' }
37 | })
38 |
39 | assert.strictEqual(component.getInputs().name.props.options.disabled, true)
40 | })
41 |
42 | test('manages refs correctly', (assert) => {
43 | assert.plan(4)
44 |
45 | const modelA = t.struct({
46 | name: t.String
47 | })
48 | const modelB = modelA.extend({
49 | age: t.Number
50 | })
51 |
52 | let formRef = null
53 | const setRef = ref => {
54 | formRef = ref
55 | }
56 |
57 | const formProps = {
58 | key: 'form',
59 | ref: setRef,
60 | value: {
61 | name: 'test',
62 | age: 5
63 | }
64 | }
65 |
66 | const formB = React.createElement(Form, {
67 | ...formProps,
68 | type: modelB,
69 | })
70 |
71 | const renderer = ReactTestRenderer.create(formB)
72 |
73 | assert.strictEqual(Object.keys(formRef.inputRef.childRefs).length, 2, 'should have 2 ref keys')
74 | assert.strictEqual(formRef.validate().errors.length, 0, 'validation works')
75 |
76 | const formA = React.createElement(Form, {
77 | ...formProps,
78 | type: modelA,
79 | })
80 |
81 | renderer.update(formA)
82 |
83 | assert.strictEqual(Object.keys(formRef.inputRef.childRefs).length, 1, 'should have 1 ref keys')
84 | assert.strictEqual(formRef.validate().errors.length, 0, 'validation works')
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/test/components/Textbox.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import t from 'tcomb-validation'
3 | import bootstrap from 'tcomb-form-templates-bootstrap'
4 | import React from 'react'
5 | import { Textbox } from '../../src/components'
6 | import { ctx, ctxPlaceholders, ctxNone, getRenderComponent } from './util'
7 | const renderComponent = getRenderComponent(Textbox)
8 |
9 | const transformer = {
10 | format: (value) => Array.isArray(value) ? value : value.split(' '),
11 | parse: (value) => value.join(' ')
12 | }
13 |
14 | tape('Textbox', ({ test }) => {
15 | test('path', (assert) => {
16 | assert.plan(1)
17 |
18 | assert.deepEqual(
19 | new Textbox({
20 | type: t.Str,
21 | options: {},
22 | ctx: ctx
23 | }).getLocals().path,
24 | [ 'defaultPath' ],
25 | 'should handle the path')
26 | })
27 |
28 | test('attrs', (assert) => {
29 | assert.plan(1)
30 |
31 | assert.strictEqual(
32 | new Textbox({
33 | type: t.Num,
34 | options: {
35 | type: 'number',
36 | attrs: {
37 | min: 0
38 | }
39 | },
40 | ctx: ctx
41 | }).getLocals().attrs.min,
42 | 0,
43 | 'should handle attrs option')
44 | })
45 |
46 | test('attrs.events', (assert) => {
47 | assert.plan(1)
48 |
49 | function onBlur() {}
50 |
51 | assert.deepEqual(
52 | new Textbox({
53 | type: t.Str,
54 | options: {
55 | attrs: {
56 | id: 'myid',
57 | onBlur: onBlur
58 | }
59 | },
60 | ctx: ctx
61 | }).getLocals().attrs,
62 | {
63 | name: 'defaultName',
64 | id: 'myid',
65 | onBlur: onBlur,
66 | placeholder: undefined
67 | },
68 | 'should handle events')
69 | })
70 |
71 | test('label', (assert) => {
72 | assert.plan(6)
73 |
74 | assert.strictEqual(
75 | new Textbox({
76 | type: t.Str,
77 | options: {},
78 | ctx: ctx
79 | }).getLocals().label,
80 | 'Default label',
81 | 'should have a default label')
82 |
83 | ctx.i18n.required = ' (required)'
84 | assert.strictEqual(
85 | new Textbox({
86 | type: t.Str,
87 | options: {},
88 | ctx: ctx
89 | }).getLocals().label,
90 | 'Default label (required)',
91 | 'should have a default label')
92 | ctx.i18n.required = ''
93 |
94 | assert.strictEqual(
95 | new Textbox({
96 | type: t.Str,
97 | options: {label: 'mylabel'},
98 | ctx: ctx
99 | }).getLocals().label,
100 | 'mylabel',
101 | 'should handle label option as string')
102 |
103 | const actual = new Textbox({
104 | type: t.Str,
105 | options: {label: React.DOM.i(null, 'JSX label')},
106 | ctx: ctx
107 | }).getLocals().label
108 | assert.equal(actual.type, 'i')
109 | assert.equal(actual.props.children, 'JSX label')
110 |
111 | assert.strictEqual(
112 | new Textbox({
113 | type: t.maybe(t.Str),
114 | options: {},
115 | ctx: ctx
116 | }).getLocals().label,
117 | 'Default label (optional)',
118 | 'should handle optional types')
119 | })
120 |
121 | test('attrs.placeholder', (assert) => {
122 | assert.plan(6)
123 |
124 | assert.strictEqual(
125 | new Textbox({
126 | type: t.Str,
127 | options: {},
128 | ctx: ctx
129 | }).getLocals().attrs.placeholder,
130 | undefined,
131 | 'default placeholder should be undefined')
132 |
133 | assert.strictEqual(
134 | new Textbox({
135 | type: t.Str,
136 | options: {attrs: {placeholder: 'myplaceholder'}},
137 | ctx: ctx
138 | }).getLocals().attrs.placeholder,
139 | 'myplaceholder',
140 | 'should handle placeholder option')
141 |
142 | assert.strictEqual(
143 | new Textbox({
144 | type: t.Str,
145 | options: {label: 'mylabel', attrs: {placeholder: 'myplaceholder'}},
146 | ctx: ctx
147 | }).getLocals().attrs.placeholder,
148 | 'myplaceholder',
149 | 'should handle placeholder option even if a label is specified')
150 |
151 | assert.strictEqual(
152 | new Textbox({
153 | type: t.Str,
154 | options: {},
155 | ctx: ctxPlaceholders
156 | }).getLocals().attrs.placeholder,
157 | 'Default label',
158 | 'should have a default placeholder if auto = placeholders')
159 |
160 | assert.strictEqual(
161 | new Textbox({
162 | type: t.maybe(t.Str),
163 | options: {},
164 | ctx: ctxPlaceholders
165 | }).getLocals().attrs.placeholder,
166 | 'Default label (optional)',
167 | 'should handle optional types if auto = placeholders')
168 |
169 | assert.strictEqual(
170 | new Textbox({
171 | type: t.Str,
172 | options: {attrs: {placeholder: 'myplaceholder'}},
173 | ctx: ctxNone
174 | }).getLocals().attrs.placeholder,
175 | 'myplaceholder',
176 | 'should handle placeholder option even if auto === none')
177 | })
178 |
179 | test('disabled', (assert) => {
180 | assert.plan(3)
181 |
182 | assert.strictEqual(
183 | new Textbox({
184 | type: t.Str,
185 | options: {},
186 | ctx: ctx
187 | }).getLocals().disabled,
188 | undefined,
189 | 'default disabled should be undefined')
190 |
191 | assert.strictEqual(
192 | new Textbox({
193 | type: t.Str,
194 | options: {disabled: true},
195 | ctx: ctx
196 | }).getLocals().disabled,
197 | true,
198 | 'should handle disabled = true')
199 |
200 | assert.strictEqual(
201 | new Textbox({
202 | type: t.Str,
203 | options: {disabled: false},
204 | ctx: ctx
205 | }).getLocals().disabled,
206 | false,
207 | 'should handle disabled = false')
208 | })
209 |
210 | test('help', (assert) => {
211 | assert.plan(3)
212 |
213 | assert.strictEqual(
214 | new Textbox({
215 | type: t.Str,
216 | options: {help: 'myhelp'},
217 | ctx: ctx
218 | }).getLocals().help,
219 | 'myhelp',
220 | 'should handle help option as string')
221 |
222 | const actual = new Textbox({
223 | type: t.Str,
224 | options: {help: React.DOM.i(null, 'JSX help')},
225 | ctx: ctx
226 | }).getLocals().help
227 | assert.equal(actual.type, 'i')
228 | assert.equal(actual.props.children, 'JSX help')
229 | })
230 |
231 | test('value', (assert) => {
232 | assert.plan(3)
233 |
234 | assert.strictEqual(
235 | new Textbox({
236 | type: t.Str,
237 | options: {},
238 | ctx: ctx
239 | }).getLocals().value,
240 | '',
241 | 'default value should be \'\'')
242 |
243 | assert.strictEqual(
244 | new Textbox({
245 | type: t.Str,
246 | options: {},
247 | ctx: ctx,
248 | value: 'a'
249 | }).getLocals().value,
250 | 'a',
251 | 'should handle value option')
252 |
253 | assert.strictEqual(
254 | new Textbox({
255 | type: t.Num,
256 | options: {},
257 | ctx: ctx,
258 | value: 1.1
259 | }).getLocals().value,
260 | '1.1',
261 | 'should handle numeric values')
262 | })
263 |
264 | test('transformer', (assert) => {
265 | assert.plan(13)
266 |
267 | assert.deepEqual(
268 | new Textbox({
269 | type: t.Str,
270 | options: {transformer: transformer},
271 | ctx: ctx,
272 | value: 'a b'
273 | }).getLocals().value,
274 | ['a', 'b'],
275 | 'should handle transformer option (format)')
276 |
277 | const { parse } = Textbox.numberTransformer
278 | assert.equal(parse(''), null)
279 | assert.equal(parse(' \r\n\t'), null)
280 | assert.equal(parse('a'), 'a')
281 | assert.equal(parse('1'), 1)
282 | assert.equal(parse('1.2a'), '1.2a')
283 | assert.equal(parse('1.a2'), '1.a2')
284 | assert.equal(parse('1a2'), '1a2')
285 | assert.equal(parse('1 2'), '1 2')
286 | assert.equal(parse(true), true)
287 | assert.equal(parse(null), null)
288 | assert.equal(parse(undefined), null)
289 | const objValue = { foo: 'bar' }
290 | assert.equal(parse(objValue), objValue)
291 | })
292 |
293 | test('hasError', (assert) => {
294 | assert.plan(2)
295 |
296 | assert.strictEqual(
297 | new Textbox({
298 | type: t.Str,
299 | options: {},
300 | ctx: ctx
301 | }).getLocals().hasError,
302 | false,
303 | 'default hasError should be false')
304 |
305 | assert.strictEqual(
306 | new Textbox({
307 | type: t.Str,
308 | options: {hasError: true},
309 | ctx: ctx
310 | }).getLocals().hasError,
311 | true,
312 | 'should handle hasError option')
313 | })
314 |
315 | test('error', (assert) => {
316 | assert.plan(3)
317 |
318 | assert.strictEqual(
319 | new Textbox({
320 | type: t.Str,
321 | options: {},
322 | ctx: ctx
323 | }).getLocals().error,
324 | undefined,
325 | 'default error should be undefined')
326 |
327 | assert.strictEqual(
328 | new Textbox({
329 | type: t.Str,
330 | options: {error: 'myerror', hasError: true},
331 | ctx: ctx
332 | }).getLocals().error,
333 | 'myerror',
334 | 'should handle error option')
335 |
336 | assert.strictEqual(
337 | new Textbox({
338 | type: t.Str,
339 | options: {
340 | error: (value) => 'error: ' + value,
341 | hasError: true
342 | },
343 | ctx: ctx,
344 | value: 'a'
345 | }).getLocals().error,
346 | 'error: a',
347 | 'should handle error option as a function')
348 | })
349 |
350 | test('template', (assert) => {
351 | assert.plan(2)
352 |
353 | assert.strictEqual(
354 | new Textbox({
355 | type: t.Str,
356 | options: {},
357 | ctx: ctx
358 | }).getTemplate(),
359 | bootstrap.textbox,
360 | 'default template should be bootstrap.textbox')
361 |
362 | const template = () => {}
363 |
364 | assert.strictEqual(
365 | new Textbox({
366 | type: t.Str,
367 | options: {template: template},
368 | ctx: ctx
369 | }).getTemplate(),
370 | template,
371 | 'should handle template option')
372 | })
373 |
374 | if (typeof window !== 'undefined') {
375 | test('validate', (assert) => {
376 | assert.plan(20)
377 |
378 | let result
379 |
380 | // required type, default value
381 | result = renderComponent({
382 | type: t.Str
383 | }).validate()
384 |
385 | assert.strictEqual(result.isValid(), false)
386 | assert.strictEqual(result.value, null)
387 |
388 | // required type, setting a value
389 | result = renderComponent({
390 | type: t.Str,
391 | value: 'a'
392 | }).validate()
393 |
394 | assert.strictEqual(result.isValid(), true)
395 | assert.strictEqual(result.value, 'a')
396 |
397 | // string type with numeric value
398 | result = renderComponent({
399 | type: t.Str,
400 | value: '123'
401 | }).validate()
402 |
403 | assert.strictEqual(result.isValid(), true)
404 | assert.strictEqual(result.value, '123')
405 |
406 | // optional type
407 | result = renderComponent({
408 | type: t.maybe(t.Str)
409 | }).validate()
410 |
411 | assert.strictEqual(result.isValid(), true)
412 | assert.strictEqual(result.value, null)
413 |
414 | // numeric type
415 | result = renderComponent({
416 | type: t.Num,
417 | value: 1
418 | }).validate()
419 |
420 | assert.strictEqual(result.isValid(), true)
421 | assert.strictEqual(result.value, 1)
422 |
423 | // optional numeric type
424 | result = renderComponent({
425 | type: t.maybe(t.Num),
426 | value: ''
427 | }).validate()
428 |
429 | assert.strictEqual(result.isValid(), true)
430 | assert.strictEqual(result.value, null)
431 |
432 | // numeric type with stringy value
433 | result = renderComponent({
434 | type: t.Num,
435 | value: '1.01'
436 | }).validate()
437 |
438 | assert.strictEqual(result.isValid(), true)
439 | assert.strictEqual(result.value, 1.01)
440 |
441 | // subtype, setting a valid value
442 | result = renderComponent({
443 | type: t.subtype(t.Num, (n) => n >= 0),
444 | value: 1
445 | }).validate()
446 |
447 | assert.strictEqual(result.isValid(), true)
448 | assert.strictEqual(result.value, 1)
449 |
450 | // subtype, setting an invalid value
451 | result = renderComponent({
452 | type: t.subtype(t.Num, (n) => n >= 0),
453 | value: -1
454 | }).validate()
455 |
456 | assert.strictEqual(result.isValid(), false)
457 | assert.strictEqual(result.value, -1)
458 |
459 | // should handle transformer option (parse)
460 | result = renderComponent({
461 | type: t.Str,
462 | options: {transformer: transformer},
463 | value: ['a', 'b']
464 | }).validate()
465 |
466 | assert.strictEqual(result.isValid(), true)
467 | assert.strictEqual(result.value, 'a b')
468 | })
469 | }
470 | })
471 |
--------------------------------------------------------------------------------
/test/components/index.js:
--------------------------------------------------------------------------------
1 | import './Component'
2 | import './Textbox'
3 | import './Checkbox'
4 | import './Select'
5 | import './Radio'
6 | import './Datetime'
7 | import './Struct'
8 | import './List'
9 |
--------------------------------------------------------------------------------
/test/components/util.js:
--------------------------------------------------------------------------------
1 | import t from 'tcomb-validation'
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import bootstrap from 'tcomb-form-templates-bootstrap'
5 | import { UIDGenerator } from '../../src/util'
6 |
7 | const ctx = {
8 | uidGenerator: new UIDGenerator('root'),
9 | auto: 'labels',
10 | config: {},
11 | name: 'defaultName',
12 | label: 'Default label',
13 | i18n: {
14 | optional: ' (optional)',
15 | required: '',
16 | add: 'Add',
17 | remove: 'Remove',
18 | up: 'Up',
19 | down: 'Down'
20 | },
21 | templates: bootstrap,
22 | path: ['defaultPath']
23 | }
24 |
25 | function getContext(options) {
26 | return t.mixin(t.mixin({}, ctx), options, true)
27 | }
28 |
29 | const ctxPlaceholders = getContext({auto: 'placeholders'})
30 | const ctxNone = getContext({auto: 'none'})
31 |
32 | function getRenderComponent(Component) {
33 | return (props) => {
34 | props.options = props.options || {}
35 | props.ctx = props.ctx || ctx
36 | const node = document.createElement('div')
37 | document.body.appendChild(node)
38 | return ReactDOM.render(React.createElement(Component, props), node)
39 | }
40 | }
41 |
42 | export default {
43 | ctx,
44 | ctxPlaceholders,
45 | ctxNone,
46 | getRenderComponent
47 | }
48 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import './util'
2 | import './components'
3 |
--------------------------------------------------------------------------------
/test/util.js:
--------------------------------------------------------------------------------
1 | import tape from 'tape'
2 | import { humanize, move, isArraysShallowDiffers } from '../src/util'
3 |
4 | tape('util', ({ test }) => {
5 | test('humanize', (assert) => {
6 | assert.plan(1)
7 | assert.strictEqual(humanize('birthDate'), 'Birth date')
8 | })
9 |
10 | test('move', (assert) => {
11 | assert.plan(2)
12 | const initial = [1, 2, 3]
13 | const actual = move(initial, 1, 2)
14 | const expected = [1, 3, 2]
15 | assert.strictEqual(initial, actual)
16 | assert.deepEqual(actual, expected)
17 | })
18 |
19 | test('isArraysShallowDiffers', (assert) => {
20 | assert.plan(4)
21 |
22 | const array = ['foo', 1]
23 | let other = ['bar', 1]
24 | assert.equal(isArraysShallowDiffers(array, other), true)
25 |
26 | other[0] = 'foo'
27 | assert.equal(isArraysShallowDiffers(array, other), false)
28 |
29 | other.push('baz')
30 | assert.equal(isArraysShallowDiffers(array, other), true)
31 |
32 | other = array
33 | assert.equal(isArraysShallowDiffers(array, other), false)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/webpack.dist.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var path = require('path');
4 | var library = 'TcombForm';
5 |
6 | module.exports = [
7 | {
8 | devtool: 'source-map',
9 | entry: './lib/main.js',
10 | output: {
11 | path: path.resolve(__dirname, './dist'),
12 | filename: 'tcomb-form.js',
13 | library: library,
14 | libraryTarget: 'umd'
15 | },
16 | externals: {
17 | 'react': 'React'
18 | },
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js?$/,
23 | loader: 'babel',
24 | query: {
25 | stage: 0,
26 | loose: true
27 | },
28 | exclude: [/node_modules/]
29 | }
30 | ]
31 | },
32 | plugins: [
33 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development') })
34 | ]
35 | },
36 | {
37 | devtool: 'source-map',
38 | entry: './lib/main.js',
39 | output: {
40 | path: path.resolve(__dirname, './dist'),
41 | filename: 'tcomb-form.min.js',
42 | library: library,
43 | libraryTarget: 'umd'
44 | },
45 | externals: {
46 | 'react': 'React'
47 | },
48 | module: {
49 | loaders: [
50 | {
51 | test: /\.js?$/,
52 | loader: 'babel',
53 | query: {
54 | stage: 0,
55 | loose: true
56 | },
57 | exclude: [/node_modules/]
58 | }
59 | ]
60 | },
61 | plugins: [
62 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }),
63 | new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })
64 | ]
65 | }
66 | ];
67 |
--------------------------------------------------------------------------------