Table of Contents
- Get started
- Types
- Rendering options
- Customizations
- Bootstrap extras
- General configuration
$ npm install tcomb-form
Note: Use [email protected] with [email protected]. See #200.
import React from 'react';
import { render } from 'react-dom';
import t from 'tcomb-form';
const Form = t.form.Form;
// define your domain model with tcomb
// https://github.com/gcanti/tcomb
const Person = t.struct({
name: t.String,
surname: t.String
});
const App = React.createClass({
save() {
// call getValue() to get the values of the form
var value = this.refs.form.getValue();
// if validation fails, value will be null
if (value) {
// value here is an instance of Person
console.log(value);
}
},
render() {
return (
<div>
<Form
ref="form"
type={Person}
/>
<button onClick={this.save}>Save</button>
</div>
);
}
});
render(<App />, document.getElementById('app'));
Note. Labels are automatically generated.
Returns null
if the validation failed; otherwise returns an instance of your model.
Note. Calling
getValue
will cause the validation of all the fields of the form, including some side effects like highlighting the errors.
Returns a ValidationResult
(see tcomb-validation for a reference documentation).
The Form
component behaves like a controlled component:
const App = React.createClass({
getInitialState() {
return {
value: {
name: 'Giulio',
surname: 'Canti'
}
};
},
onChange(value) {
this.setState({value});
},
save() {
var value = this.refs.form.getValue();
if (value) {
console.log(value);
}
},
render() {
return (
<div>
<Form
ref="form"
type={Person}
value={this.state.value}
onChange={this.onChange}
/>
<button onClick={this.save}>Save</button>
</div>
);
}
});
The onChange
handler has the following signature:
(raw: any, path: Array<string | number>, kind?) => void
where
raw
contains the current raw value of the form (can be an invalid value for your model)path
is the path to the field triggering the changekind
specify the kind of change (whenundefined
means a value change)'add'
list item added'remove'
list item removed'moveUp'
list item moved up'moveDown'
list item moved down
In the previous example, just pass null
to the value
prop:
const App = React.createClass({
...
resetForm() {
this.setState({value: null});
},
save() {
var value = this.refs.form.getValue();
if (value) {
console.log(value);
// clear all fields after submit
this.resetForm();
}
},
...
});
You can get access to a field with the getComponent(path)
API:
const Person = t.struct({
name: t.String,
surname: t.String
});
const App = React.createClass({
onChange(value, path) {
// validate a field on every change
this.refs.form.getComponent(path).validate();
},
render() {
return (
<div>
<Form ref="form"
type={Person}
onChange={this.onChange}
/>
</div>
);
}
});
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:
const App = React.createClass({
onSubmit(evt) {
const value = this.refs.form.getValue();
if (!value) {
// there are errors, don't send the form
evt.preventDefault();
} else {
// everything ok, let the form fly...
// ...or handle the values contained in the
// `value` variable with js
}
},
render() {
return (
<form onSubmit={this.onSubmit}>
<Form
ref="form"
type={Person}
/>
<button type="submit">Save</button>
</form>
);
}
});
See Error messages section.
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:
const AccountType = t.enums.of([
'type 1',
'type 2',
'other'
], 'AccountType')
const KnownAccount = t.struct({
type: AccountType
}, 'KnownAccount')
// UnknownAccount extends KnownAccount so it owns also the type field
const UnknownAccount = KnownAccount.extend({
label: t.String,
}, 'UnknownAccount')
// the union
const Account = t.union([KnownAccount, UnknownAccount], 'Account')
// the final form type
const Type = t.list(Account)
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:
// if account type is 'other' return the UnknownAccount type
Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount
The complete example:
import React from 'react'
import t from 'tcomb-form'
const AccountType = t.enums.of([
'type 1',
'type 2',
'other'
], 'AccountType')
const KnownAccount = t.struct({
type: AccountType
}, 'KnownAccount')
const UnknownAccount = KnownAccount.extend({
label: t.String,
}, 'UnknownAccount')
const Account = t.union([KnownAccount, UnknownAccount], 'Account')
Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount
const Type = t.list(Account)
const App = React.createClass({
onSubmit(evt) {
evt.preventDefault()
const v = this.refs.form.getValue()
if (v) {
console.log(v)
}
},
render() {
return (
<form onSubmit={this.onSubmit}>
<t.form.Form
ref="form"
type={Type}
/>
<div className="form-group">
<button type="submit" className="btn btn-primary">Save</button>
</div>
</form>
)
}
})
Models are defined with 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.
By default fields are required:
const Person = t.struct({
name: t.String, // a required string
surname: t.String // a required string
});
In order to create an optional field, wrap the field type with the t.maybe
combinator:
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String) // an optional string
});
The suffix " (optional)"
is automatically added to optional fields.
You can customise the suffix value, or set a suffix for required fields (see the "Internationalization" section).
In order to create a numeric field, use the t.Number
type:
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number // a numeric field
});
tcomb-form will automatically convert numbers to / from strings.
A predicate is a function with the following signature:
(x: any) => boolean
You can refine a type with the t.refinement(type, predicate)
combinator:
// a type representing positive numbers
const Positive = t.refinement(t.Number, (n) => n >= 0});
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: Positive
});
Refinements allow you to express any custom validation with a simple predicate.
In order to create a boolean field, use the t.Boolean
type:
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean // a boolean field
});
Booleans are displayed as checkboxes.
In order to create a date field, use the t.Date
type:
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean,
birthDate: t.Date // a date field
});
In order to create an enum field, use the t.enums
combinator:
const Gender = t.enums({
M: 'Male',
F: 'Female'
});
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean,
birthDate: t.Date,
gender: Gender // enum
});
By default enums are displayed as selects.
You can handle a list with the t.list
combinator:
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: Positive, // refinement
rememberMe: t.Boolean,
birthDate: t.Date,
gender: Gender,
tags: t.list(t.String) // a list of strings
});
You can nest lists and structs at an arbitrary level:
const Person = t.struct({
name: t.String,
surname: t.String
});
const Persons = t.list(Person);
...
<Form
ref="form"
type={Persons}
/>
In order to customise the look and feel, use an options
prop:
<Form type={Model} options={options} />
Warning. tcomb-form uses shouldComponentUpdate aggressively. In order to ensure that tcomb-form detect any change to
type
,options
orvalue
props you have to change references:
Example: disable a field based on another field's value
const Type = t.struct({
disable: t.Boolean, // if true, name field will be disabled
name: t.String
});
const options = {
fields: {
name: {}
}
};
const App = React.createClass({
getInitialState() {
return {
options: options,
value: null
};
},
onSubmit(evt) {
evt.preventDefault();
var value = this.refs.form.getValue();
if (value) {
console.log(value);
}
},
onChange(value) {
// tcomb immutability helpers
// https://github.com/gcanti/tcomb/blob/master/GUIDE.md#updating-immutable-instances
var options = t.update(this.state.options, {
fields: {
name: {
disabled: {'$set': value.disable}
}
}
});
this.setState({options: options, value: value});
},
render() {
return (
<form onSubmit={this.onSubmit}>
<Form ref="form"
type={Type}
options={this.state.options}
value={this.state.value}
onChange={this.onChange}
/>
<button className="btn btn-primary">Save</button>
</form>
);
}
});
In order to generate default placeholders use the option auto: 'placeholders'
:
const options = {
auto: 'placeholders'
};
<Form type={Person} options={options} />
Or auto: 'none'
if you don't want neither labels nor placeholders:
const options = {
auto: 'none'
};
You can sort the fields with the order
option:
const options = {
order: ['name', 'surname', 'rememberMe', 'gender', 'age', 'email']
};
Warning: Any field that is not in this array will be omitted from the form
You can add a fieldset legend with the legend
option:
const options = {
// you can use strings or JSX
legend: <i>My form legend</i>
};
You can add an help message with the help
option:
const options = {
// you can use strings or JSX
help: <i>My form help</i>
};
You can add a custom error message with the error
option:
const options = {
error: <i>A custom error message</i> // use strings or JSX
};
error
can also be a function with the following signature:
type getValidationErrorMessage = (value, path, context) => ?(string | ReactElement)
where
value
is the (parsed) current value of the component.path
is the path of the value being validatedcontext
is the value of thecontext
prop. Also it contains a reference to the component options.
The value returned by the function will be used as error message.
If you want to show the error message onload, add the hasError
option:
const options = {
hasError: true,
error: <i>A custom error message</i>
};
Another (advanced) way to customize the error message is to add a:
getValidationErrorMessage(value, path, context) => ?(string | ReactElement)
static function to the type, where the arguments are the same as above:
const Age = t.refinement(t.Number, (n) => return n >= 18);
// if you define a getValidationErrorMessage function, it will be called on validation errors
Age.getValidationErrorMessage = (value, path, context) => {
return 'bad age, locale: ' + context.locale;
};
const Schema = t.struct({
age: Age
});
const App = React.createClass({
onSubmit(evt) {
evt.preventDefault();
const value = this.refs.form.getValue();
if (value) {
console.log(value);
}
},
render() {
return (
<form onSubmit={this.onSubmit}>
<t.form.Form
ref="form"
type={Schema}
context={{locale: 'it-IT'}}
/>
<button type="submit" className="btn btn-primary">Save</button>
</form>
);
}
});
You can even define getValidationErrorMessage
on the supertype in order to be DRY:
t.Number.getValidationErrorMessage = (value, path, context) => {
return 'bad number';
};
Age.getValidationErrorMessage = (value, path, context) => {
return 'bad age, locale: ' + context.locale;
};
You can disable the whole fieldset with the disabled
option:
const options = {
disabled: true
};
You can configure each field with the fields
option:
const options = {
fields: {
name: {
// name field configuration here..
},
surname: {
// surname field configuration here..
},
...
}
});
fields
is a hash containing a key for each field you want to configure.
You can customise the look and feel with the template
option:
const options = {
template: mytemplate // see Templates section for documentation
}
The following options are similar to the Struct ones:
auto
disabled
help
hasError
error
legend
template
To configure all the items in a list, set the item
option:
const Colors = t.list(t.String);
const options = {
item: {
type: 'color' // HTML5 type attribute
}
});
<Form type={Colors} options={options} />
You can override the default language (english) with the i18n
option:
const options = {
i18n: {
add: 'Nuovo', // add button
down: 'Giù', // move down button
optional: ' (opzionale)', // suffix added to optional fields
required: '', // suffix added to required fields
remove: 'Elimina', // remove button
up: 'Su' // move up button
}
};
You can prevent operations on lists with the following options:
disableAdd
: (defaultfalse
) prevents adding new itemsdisableRemove
: (defaultfalse
) prevents removing existing itemsdisableOrder
: (defaultfalse
) prevents sorting existing items
const options = {
disableOrder: true
};
const AccountType = t.enums.of([
'type 1',
'type 2',
'other'
], 'AccountType')
const KnownAccount = t.struct({
type: AccountType
}, 'KnownAccount')
// UnknownAccount extends KnownAccount so it owns also the type field
const UnknownAccount = KnownAccount.extend({
label: t.String,
}, 'UnknownAccount')
// the union
const Account = t.union([KnownAccount, UnknownAccount], 'Account')
// the final form type
const Type = t.list(Account)
const options = {
item: [ // one options object for each concrete type of the union
{
label: 'KnownAccount'
},
{
label: 'UnknownAccount'
}
]
}
Tech note. Values containing only white spaces are converted to null.
The following options are similar to the fieldset ones:
disabled
help
hasError
error
template
You can set the type attribute with the type
option. The following values are allowed:
- 'text' (default)
- 'password'
- 'hidden'
- 'textarea' (outputs a textarea instead of a textbox)
- 'static' (outputs a static value)
- all the HTML5 type values
You can override the default label with the label
option:
const options = {
fields: {
name: {
// you can use strings or JSX
label: <i>My label</i>
}
}
};
You can add attributes and events with the attrs
option:
const options = {
fields: {
name: {
attrs: {
autoFocus: true,
placeholder: 'Type your name here',
onBlur: () => {
console.log('onBlur');
}
}
}
}
};
You can a style class with the className
or the style
attribute:
const options = {
fields: {
name: {
attrs: {
className: 'myClassName'
}
}
}
};
className
can be a string, an array of strings or a dictionary string -> boolean
.
The following options are similar to the textbox ones:
attrs
label
help
disabled
hasError
error
template
The following options are similar to the textbox ones:
attrs
label
help
disabled
hasError
error
template
You can customise the null option with the nullOption
option:
const options = {
fields: {
gender: {
nullOption: {value: '', text: 'Choose your gender'}
}
}
};
You can remove the null option by setting the nullOption
option to false
.
Warning: when you set
nullOption = false
you must also set the Form'svalue
prop for the select field.
Tech note. A value equal to
nullOption.value
(default''
) is converted tonull
.
You can sort the options with the order
option:
const options = {
fields: {
gender: {
order: 'asc' // or 'desc'
}
}
};
You can customise the options with the options
option:
const options = {
fields: {
gender: {
options: [
{value: 'M', text: 'Maschio'},
// use `disabled: true` to disable an option
{value: 'F', text: 'Femmina', disabled: true}
]
}
}
};
An option is an object with the following structure:
{
value: string, // required
text: string, // required
disabled: ?boolean // optional, default = false
}
You can also add optgroups:
const Car = t.enums.of('Audi Chrysler Ford Renault Peugeot');
const Select = t.struct({
car: Car
});
const options = {
fields: {
car: {
options: [
{value: 'Audi', text: 'Audi'}, // an option
{label: 'US', options: [ // a group of options
{value: 'Chrysler', text: 'Chrysler'},
{value: 'Ford', text: 'Ford'}
]},
{label: 'France', options: [ // another group of options
{value: 'Renault', text: 'Renault'},
{value: 'Peugeot', text: 'Peugeot'}
], disabled: true} // use `disabled: true` to disable an optgroup
]
}
}
};
You can render the select as a radio group by using the factory
option to override the default:
const options = {
factory: t.form.Radio
};
You can turn the select into a multiple select by passing a list
as type and using the factory
option to override the default:
const Car = t.enums.of('Audi Chrysler Ford Renault Peugeot');
const Select = t.struct({
car: t.list(Car)
});
const options = {
fields: {
car: {
factory: t.form.Select
}
}
};
The following options are similar to the textbox ones:
label
help
disabled
hasError
error
template
You can sort the fields with the order
option:
const Type = t.struct({
date: t.Date
});
const options = {
fields: {
date: {
order: ['D', 'M', 'YY']
}
}
};
To customise the "skin" of tcomb-form you have to write a template. A template is simply a function with the following signature:
(locals: any) => ReactElement
where locals
is an object contaning the "recipe" for rendering the input, and is built by tcomb-form for you.
For example, this is the recipe for a textbox:
{
attrs: Object // should render attributes and events
config: Object // custom options, see Template addons section
context: Any // a reference to the context prop
disabled: maybe(Boolean), // should be disabled
error: maybe(Label), // should show an error
hasError: maybe(Boolean), // if true should show an error state
help: maybe(Label), // should show a help message
label: maybe(Label), // should show a label
onChange: Function, // should call this function with the changed value
path: Array, // the path of this field with respect to the form root
type: String, // should use this as type attribute
typeInfo: Object // an object containing info on the current type
value: Any // the current value of the textbox
}
You can set a custom template using the template
option:
const Animal = t.enums({
dog: "Dog",
cat: "Cat"
});
const Pet = t.struct({
name: t.String,
type: Animal
});
const Person = t.struct({
name: t.String,
pets: t.list(Pet)
});
const formLayout = (locals) => {
return (
<div>
<p>formLayout</p>
<div>{locals.inputs.name}</div>
<div>{locals.inputs.pets}</div>
</div>
);
};
const petLayout = (locals) => {
return (
<div>
<p>petLayout</p>
<div>{locals.inputs.name}</div>
<div>{locals.inputs.type}</div>
</div>
);
};
const options = {
template: formLayout,
fields: {
pets: { // <- pets is a list, you can customise the elements with the `item` option
item: {
template: petLayout
}
}
}
};
const value = {
name: 'myname',
pets: [
{name: 'pet1', type: 'dog'},
{name: 'pet2', type: 'cat'}
]
};
const App = React.createClass({
save() {
const value = this.refs.form.getValue();
if (value) {
console.log(value);
}
},
render() {
return (
<div>
<Form ref="form"
type={Person}
options={options}
value={value}
/>
<br/>
<button className="btn btn-primary" onClick={this.save}>Save</button>
</div>
);
}
});
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:
const Type = t.struct({
name: t.String
})
const myTemplate = t.form.Form.templates.textbox.clone({
// override just the input default implementation (labels, help, error will be preserved)
renderInput: (locals) => {
return <input value={locals.value} />
}
})
const options = {
fields: {
name: {
template: myTemplate
}
}
}
Say you want a search textbox which accepts a list of keywords separated by spaces:
const Search = t.struct({
search: t.list(t.String)
});
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:
const options = {
fields: {
search: {
factory: t.form.Textbox
}
}
};
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:
const Transformer = t.struct({
format: t.Function, // from value to string, it must be idempotent
parse: t.Function // from string to value
});
Important. the format
function SHOULD BE idempotent.
A basic transformer implementation for the search textbox:
const listTransformer = {
format: (value) => {
return Array.isArray(value) ? value.join(' ') : value;
},
parse: (str) => {
return str ? str.split(' ') : [];
}
};
Now you can handle lists using the transformer option:
// example of initial value
const value = {
search: ['climbing', 'yosemite']
};
const options = {
fields: {
search: {
factory: t.form.Textbox,
transformer: listTransformer,
help: 'Keywords are separated by spaces'
}
}
};
The easisiest way to define a custom factory is to extend t.form.Component
and define the getTemplate
method:
//
// a custom factory representing a tags input
//
import React from 'react';
import t from 'tcomb-form';
import TagsInput from 'react-tagsinput'; // I'm using this but you can build your own (and reusable!) tagsinput
class TagsComponent extends t.form.Component { // extend the base class
getTemplate() {
return (locals) => {
return (
<TagsInput value={locals.value} onChange={locals.onChange} />
);
};
}
}
export default TagsComponent;
Usage
const Type = t.struct({
tags: t.list(t.String)
});
const options = {
fields: {
tags: {
factory: TagsComponent
}
}
};
const value = {
tags: [] // react-tagsinput requires an initial value
}
...
<t.form.Form
ref="form"
type={Type}
options={options}
value={value}
/>
If a type owns a getTcombFormFactory(options)
static function, it will be used to retrieve the suitable factory
Example
// instead of
const Country = t.enums.of(['IT', 'US'], 'Country');
const Type = t.struct({
country: Country
});
const options = {
fields: {
country: {
factory: t.form.Radio
}
}
};
// you can write
const Country = t.enums.of(['IT', 'US'], 'Country');
Country.getTcombFormFactory = (/*options*/) => {
return t.form.Radio;
};
const Type = t.struct({
country: Country
});
const options = {};
If a type owns a getTcombFormOptions
static function, tcomb-form will call it and use the result to populate the rendering options.
For example, the following two code snippets behave the same:
const Message = t.maybe(t.String);
const Textbox = t.struct({
message: Message
});
Message.getTcombFormOptions = formOptions => ({
help: <span>Enter text here!</span>
});
<Form type={Textbox} />
const Textbox = t.struct({
message: t.maybe(t.String)
});
const options = {
fields: {
message: {
help: <span>Enter text here!</span>
}
}
};
<Form type={Textbox} options={options} />
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.
getTcombFormOptions
is particularly helpful to keep code easy to follow when using tcomb-form on deeply nested data structures.
You can set an addon before or an addon after with the config.addonBefore
and config.addonAfter
options:
const Textbox = t.struct({
mytext: t.String
});
const options = {
fields: {
mytext: {
config: {
// you can use strings or JSX
addonBefore: <i>before</i>,
addonAfter: <i>after</i>
}
}
}
};
<Form type={Textbox} options={options} />
You can set a button after (before) with the config.buttonAfter
(config.buttonBefore
) option:
const options = {
fields: {
mytext: {
config: {
buttonAfter: <button className="btn btn-default">Click me</button>
}
}
}
};
<Form type={Textbox} options={options} />
You can set the textbox size with the config.size
option:
const Textbox = t.struct({
mytext: t.String
});
const options = {
fields: {
mytext: {
config: {
size: 'lg' // xs sm md lg are allowed
}
}
}
};
<Form type={Textbox} options={options} />
Same as Textbox extras.
You can render the form horizontal with the config.horizontal
option:
const Person = t.struct({
name: t.String,
notifyMe: t.Boolean,
email: t.maybe(t.String)
});
const options = {
config: {
// for each of lg md sm xs you can specify the columns width
horizontal: {
md: [3, 9],
sm: [6, 6]
}
}
};
// remember to add the proper bootstrap style class
// to a wrapping div (or form) tag in order
// to get a nice layout
<div className="form-horizontal">
<Form type={Person} options={options} />
</div>
tcomb-form uses the following default settings:
- English as language
- Bootstrap as theme (tcomb-form-templates-bootstrap)
import t from 'tcomb-form/lib'; // load tcomb-form without templates and i18n
import templates from 'tcomb-form-templates-bootstrap';
t.form.Form.templates = templates;
t.form.Form.i18n = {
optional: ' (opzionale)',
required: '',
add: 'Nuovo',
remove: 'Elimina',
up: 'Su',
down: 'Giù'
};
Or pick one in the i18n
folder (constributions welcome!):
import t from 'tcomb-form/lib'; // load tcomb-form without templates and i18n
import templates from 'tcomb-form-templates-bootstrap';
import i18n from 'tcomb-form/lib/i18n/it';
t.form.Form.templates = templates;
t.form.Form.i18n = i18n;
import t from 'tcomb-form/lib'; // load tcomb-form without templates and i18n
import i18n from 'tcomb-form/lib/i18n/en';
import semantic from 'tcomb-form-templates-semantic';
t.form.Form.i18n = i18n;
t.form.Form.templates = semantic;