@riebel/tcomb-form-native-ts
v1.1.8
Published
React Native forms powered by tcomb-validation. Modern TypeScript build with legacy-compatible API (drop-in replacement).
Maintainers
Readme
@riebel/tcomb-form-native-ts (TypeScript Edition)
A modern TypeScript implementation of tcomb-form-native with React 18+ support, functional components, and 100% API compatibility with the original library.
Contents
- Setup
- Supported React Native Versions
- Example
- API
- Types
- Rendering Options
- Unions
- Lists
- Customizations
- Migration Guide
- Tests
- License
Setup
npm install @riebel/tcomb-form-native-tsFor React Native projects with Expo:
npx expo install @react-native-picker/pickerFor bare React Native projects:
npm install @react-native-picker/picker
npx react-native link @react-native-picker/pickerSupported React Native Versions
| Version | React Native Support | TypeScript | React | |---------|---------------------|------------|-------| | 1.1.x | 0.60.0+ | 4.5+ | 18.0+ |
This library uses modern React patterns including hooks and functional components.
Domain Driven Forms
The tcomb library provides a concise but expressive way to define domain models in JavaScript/TypeScript.
The tcomb-validation library builds on tcomb, providing validation functions for tcomb domain models.
This library builds on those two and React Native, providing a modern TypeScript implementation with full backward compatibility.
Benefits
With tcomb-form-native you simply call <Form type={Model} /> to generate a form based on that domain model. What does this get you?
- Write a lot less code
- Usability and accessibility for free (automatic labels, inline validation, etc)
- No need to update forms when domain model changes
- Full TypeScript support with type safety
- Modern React patterns (hooks, functional components)
JSON Schema Support
JSON Schemas are also supported via the tcomb-json-schema library.
Note: Please use tcomb-json-schema ^0.2.5.
Pluggable Look and Feel
The look and feel is customizable via React Native stylesheets and templates (see documentation).
Example
// App.tsx
import React, { useRef, useState } from 'react';
import { View, Text, TouchableHighlight, StyleSheet, Alert } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';
const Form = t.form.Form;
// Define your domain model
const Person = t.struct({
name: t.String, // a required string
surname: t.maybe(t.String), // an optional string
age: t.Number, // a required number
rememberMe: t.Boolean // a boolean
});
const options = {}; // optional rendering options (see documentation)
export default function App() {
const formRef = useRef<t.form.Form>(null);
const handlePress = () => {
// call getValue() to get the values of the form
const value = formRef.current?.getValue();
if (value) { // if validation fails, value will be null
console.log(value); // value here is an instance of Person
Alert.alert('Success', `Hello ${value.name}!`);
}
};
return (
<View style={styles.container}>
<Form
ref={formRef}
type={Person}
options={options}
/>
<TouchableHighlight
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
<Text style={styles.buttonText}>Save</Text>
</TouchableHighlight>
</View>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
marginTop: 50,
padding: 20,
backgroundColor: '#ffffff',
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
button: {
height: 36,
backgroundColor: '#48BBEC',
borderColor: '#48BBEC',
borderWidth: 1,
borderRadius: 8,
marginBottom: 10,
alignSelf: 'stretch',
justifyContent: 'center'
}
});Output:
(Labels are automatically generated)
Output after a validation error:
The form will highlight validation errors automatically.
API
getValue()
Returns null if the validation failed, an instance of your model otherwise.
Note: Calling
getValuewill cause the validation of all the fields of the form, including some side effects like highlighting the errors.
validate()
Returns a ValidationResult (see tcomb-validation for reference documentation).
Adding a Default Value and Listen to Changes
The Form component behaves like a controlled component:
import React, { useRef, useState } from 'react';
import { View, Text, TouchableHighlight } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';
const Person = t.struct({
name: t.String,
surname: t.maybe(t.String)
});
export default function App() {
const formRef = useRef<t.form.Form>(null);
const [value, setValue] = useState({
name: 'Giulio',
surname: 'Canti'
});
const handleChange = (newValue: unknown) => {
setValue(newValue);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
}
};
return (
<View style={styles.container}>
<t.form.Form
ref={formRef}
type={Person}
value={value}
onChange={handleChange}
/>
<TouchableHighlight
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
<Text style={styles.buttonText}>Save</Text>
</TouchableHighlight>
</View>
);
}The onChange handler has the following signature:
(raw: unknown, path: Array<string | number>) => voidwhere:
rawcontains the current raw value of the form (can be an invalid value for your model)pathis the path to the field triggering the change
Disable a Field Based on Another Field's Value
import React, { useRef, useState } from 'react';
import { View, TouchableHighlight, Text } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';
const Type = t.struct({
disable: t.Boolean, // if true, name field will be disabled
name: t.String
});
export default function App() {
const formRef = useRef<t.form.Form>(null);
const [value, setValue] = useState(null);
const [options, setOptions] = useState({
fields: {
name: {}
}
});
const handleChange = (newValue: unknown) => {
// Update options based on form value
const newOptions = {
...options,
fields: {
...options.fields,
name: {
...options.fields.name,
editable: !newValue?.disable
}
}
};
setOptions(newOptions);
setValue(newValue);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
}
};
return (
<View style={styles.container}>
<t.form.Form
ref={formRef}
type={Type}
options={options}
value={value}
onChange={handleChange}
/>
<TouchableHighlight
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
<Text style={styles.buttonText}>Save</Text>
</TouchableHighlight>
</View>
);
}How to Clear Form After Submit
import React, { useRef, useState } from 'react';
import { View, TouchableHighlight, Text } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';
const Person = t.struct({
name: t.String,
surname: t.maybe(t.String),
age: t.Number,
rememberMe: t.Boolean
});
export default function App() {
const formRef = useRef<t.form.Form>(null);
const [value, setValue] = useState(null);
const handleChange = (newValue: unknown) => {
setValue(newValue);
};
const clearForm = () => {
// clear content from all fields
setValue(null);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
// clear all fields after submit
clearForm();
}
};
return (
<View style={styles.container}>
<t.form.Form
ref={formRef}
type={Person}
value={value}
onChange={handleChange}
/>
<TouchableHighlight
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
<Text style={styles.buttonText}>Save</Text>
</TouchableHighlight>
</View>
);
}Dynamic Forms: Change Form Based on Selection
import React, { useRef, useState, useMemo } from 'react';
import { View, TouchableHighlight, Text } from 'react-native';
import t from '@riebel/tcomb-form-native-ts';
const Country = t.enums({
'IT': 'Italy',
'US': 'United States'
}, 'Country');
export default function App() {
const formRef = useRef<t.form.Form>(null);
const [value, setValue] = useState<Record<string, unknown>>({});
// Returns the suitable type based on the form value
const getType = (formValue: Record<string, unknown>) => {
if (formValue.country === 'IT') {
return t.struct({
country: Country,
rememberMe: t.Boolean
});
} else if (formValue.country === 'US') {
return t.struct({
country: Country,
name: t.String
});
} else {
return t.struct({
country: Country
});
}
};
const type = useMemo(() => getType(value), [value]);
const handleChange = (newValue: Record<string, unknown>) => {
setValue(newValue);
};
const handlePress = () => {
const formValue = formRef.current?.getValue();
if (formValue) {
console.log(formValue);
}
};
return (
<View style={styles.container}>
<t.form.Form
ref={formRef}
type={type}
value={value}
onChange={handleChange}
/>
<TouchableHighlight
style={styles.button}
onPress={handlePress}
underlayColor="#99d9f4"
>
<Text style={styles.buttonText}>Save</Text>
</TouchableHighlight>
</View>
);
}Types
Required Field
By default fields are required:
const Person = t.struct({
name: t.String, // a required string
surname: t.String // a required string
});Optional Field
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 postfix " (optional)" is automatically added to optional fields.
You can customize the postfix value or set a postfix for required fields:
t.form.Form.i18n = {
optional: '',
required: ' (required)' // inverting the behavior: adding a postfix to required fields
};Numbers
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-native will convert automatically numbers to/from strings.
Booleans
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.
Dates
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,
birthDate: t.Date // a date field
});Dates are displayed as date pickers on both iOS and Android.
Enums
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,
gender: Gender // enum
});Enums are displayed as Picker components.
Refinements
A predicate is a function with the following signature:
(x: unknown) => booleanYou can refine a type with the t.refinement(type, predicate) combinator:
// a type representing positive numbers
const Positive = t.refinement(t.Number, (n: number) => {
return n >= 0;
});
const Person = t.struct({
name: t.String,
surname: t.String,
email: t.maybe(t.String),
age: Positive, // refinement
rememberMe: t.Boolean,
gender: Gender
});Subtypes allow you to express custom validation with a simple predicate.
Rendering Options
In order to customize the look and feel, use an options prop:
<Form type={Model} options={options} />Form Component
Labels and Placeholders
By default labels are automatically generated. You can turn off this behavior or override the default labels on a field basis.
const options = {
label: 'My struct label' // <= form legend, displayed before the fields
};
const options = {
fields: {
name: {
label: 'My name label' // <= label for the name field
}
}
};In order to automatically generate default placeholders, use the option auto: 'placeholders':
const options = {
auto: 'placeholders'
};Set auto: 'none' if you don't want neither labels nor placeholders.
const options = {
auto: 'none'
};Fields Order
You can sort the fields with the order option:
const options = {
order: ['name', 'surname', 'rememberMe', 'gender', 'age', 'email']
};Default Values
You can set the default values by passing a value prop to the Form component:
const value = {
name: 'Giulio',
surname: 'Canti',
age: 41,
gender: 'M'
};
<Form type={Model} value={value} />Fields Configuration
You can configure each field with the fields option:
const options = {
fields: {
name: {
// name field configuration here..
},
surname: {
// surname field configuration here..
}
}
};Textbox Component
Implementation: TextInput
Tech note: Values containing only white spaces are converted to null.
Placeholder
const options = {
fields: {
name: {
placeholder: 'Your placeholder here'
}
}
};Label
const options = {
fields: {
name: {
label: 'Insert your name'
}
}
};Help Message
const options = {
fields: {
name: {
help: 'Your help message here'
}
}
};Error Messages
const options = {
fields: {
email: {
error: 'Insert a valid email'
}
}
};error can also be a function:
(value: unknown, path: string[], context: Record<string, unknown>) => string | nullStandard TextInput Options
The following standard options are available:
allowFontScalingautoCapitalizeautoCorrectautoFocusclearButtonModeeditableenablesReturnKeyAutomaticallykeyboardTypemaxLengthmultilineonBluronEndEditingonFocusonSubmitEditingonContentSizeChangeplaceholderTextColorreturnKeyTypeselectTextOnFocussecureTextEntryselectionStatetextAligntextAlignVertical
Select Component
Implementation: Picker from @react-native-picker/picker
nullOption
const options = {
fields: {
gender: {
nullOption: {value: '', text: 'Choose your gender'}
}
}
};You can remove the null option by setting nullOption to false.
Options Order
const options = {
fields: {
gender: {
order: 'asc' // or 'desc'
}
}
};DatePicker Component
Implementation: Platform-specific date picker
const Person = t.struct({
name: t.String,
birthDate: t.Date
});Hidden Component
For every component, you can set the field with the hidden option:
const options = {
fields: {
name: {
hidden: true
}
}
};Lists
You can handle a list with the t.list combinator:
const Person = t.struct({
name: t.String,
tags: t.list(t.String) // a list of strings
});Items Configuration
To configure all the items in a list, set the item option:
const Person = t.struct({
name: t.String,
tags: t.list(t.String)
});
const options = {
fields: {
tags: {
item: {
label: 'My tag'
}
}
}
};Nested Structures
const Person = t.struct({
name: t.String,
surname: t.String
});
const Persons = t.list(Person);Internationalization
const options = {
i18n: {
optional: ' (optional)',
required: '',
add: 'Add', // add button
remove: '✘', // remove button
up: '↑', // move up button
down: '↓' // move down button
}
};Buttons Configuration
const options = {
disableAdd: false, // prevents adding new items
disableRemove: false, // prevents removing existing items
disableOrder: false // prevents sorting existing items
};Unions
const AccountType = t.enums({
'type1': 'Type 1',
'type2': 'Type 2',
'other': 'Other'
}, 'AccountType');
const KnownAccount = t.struct({
type: AccountType
}, 'KnownAccount');
const UnknownAccount = KnownAccount.extend({
label: t.String,
}, 'UnknownAccount');
const Account = t.union([KnownAccount, UnknownAccount], 'Account');
// Dispatch function to select the correct type
Account.dispatch = (value: Record<string, unknown>) =>
value && value.type === 'other' ? UnknownAccount : KnownAccount;
const Type = t.list(Account);
const options = {
item: [
{ label: 'KnownAccount' },
{ label: 'UnknownAccount' }
]
};Customizations
Stylesheets
You can customize the look and feel by setting a custom stylesheet:
import t from '@riebel/tcomb-form-native-ts';
// Define a custom stylesheet
const customStylesheet = {
// ... your styles here
};
// Override globally
t.form.Form.stylesheet = customStylesheet;You can also override the stylesheet locally:
const options = {
stylesheet: myCustomStylesheet
};Or per field:
const options = {
fields: {
name: {
stylesheet: myCustomStylesheet
}
}
};Templates
You can customize the layout by setting custom templates:
import t from '@riebel/tcomb-form-native-ts';
const customTemplates = {
// ... your templates here
};
// Override globally
t.form.Form.templates = customTemplates;Local template override:
function myCustomTemplate(locals: Record<string, unknown>) {
return (
<View style={containerStyle}>
<Text style={labelStyle}>{locals.label}</Text>
<TextInput style={textboxStyle} />
</View>
);
}
const options = {
fields: {
name: {
template: myCustomTemplate
}
}
};Transformers
Transformers handle serialization/deserialization of data:
interface Transformer {
format: (value: unknown) => unknown; // from value to string
parse: (value: unknown) => unknown; // from string to value
}Example for a search field that accepts space-separated keywords:
const listTransformer = {
format: (value: string[]) => {
return Array.isArray(value) ? value.join(' ') : value;
},
parse: (str: string) => {
return str ? str.split(' ') : [];
}
};
const options = {
fields: {
search: {
factory: t.form.Textbox,
transformer: listTransformer,
help: 'Keywords are separated by spaces'
}
}
};Migration from tcomb-form-native
This package is a 100% drop-in replacement for the original tcomb-form-native. No code changes are required!
Step 1: Uninstall the old package
npm uninstall tcomb-form-native
# or
yarn remove tcomb-form-nativeStep 2: Install the new TypeScript package
npm install @riebel/tcomb-form-native-ts
# or
yarn add @riebel/tcomb-form-native-tsStep 3: Zero-code migration via package.json (Recommended)
For the smoothest migration with absolutely no code changes, use npm package aliasing in your package.json:
{
"dependencies": {
"tcomb-form-native": "npm:@riebel/tcomb-form-native-ts@^1.1.8"
}
}That's it! Now all your existing imports work without any changes:
// This continues to work exactly the same - no code changes needed!
import t from 'tcomb-form-native';
const Form = t.form.Form;The npm: prefix tells npm/yarn to install @riebel/tcomb-form-native-ts but make it available as tcomb-form-native in your project.
Alternative: Update imports manually (optional)
If you prefer not to use aliases, you can update your imports:
// Updated import
import t from '@riebel/tcomb-form-native-ts';
const Form = t.form.Form;Step 4: Install peer dependencies
Make sure you have the required peer dependencies:
# For Expo projects
npx expo install @react-native-picker/picker
# For bare React Native projects
npm install @react-native-picker/picker
npx react-native link @react-native-picker/pickerThat's it! 🎉
Your existing code will work without any changes. The new package provides:
- ✅ 100% API compatibility - All existing code works unchanged
- ✅ Modern TypeScript support - Full type safety and IntelliSense
- ✅ React 18+ compatibility - Uses modern React patterns internally
- ✅ Functional components - Modernized implementation under the hood
- ✅ Better performance - Optimized with React.memo and hooks
- ✅ Active maintenance - Regular updates and bug fixes
Optional: Modernize Your Code
While not required, you can gradually modernize your existing code:
- Convert class components to functional components with hooks
- Add TypeScript type annotations
- Use modern JavaScript syntax (
const/letinstead ofvar) - Take advantage of improved TypeScript IntelliSense
Tests
npm run testCredits
This TypeScript modernization is maintained by Hagen Sommerkorn (@riebel).
Original tcomb-form-native library created by Giulio Canti (@gcanti).
Special thanks to all the contributors who made the original library possible.
