Form state and validation (FormState, Validators) are part of tamboui-widgets and work at any API level.
The formField() and form() builders shown here are Toolkit DSL conveniences—see Toolkit DSL for an introduction.
|
TamboUI provides high-level form abstractions to simplify building interactive forms with labels, validation, and centralized state management.
Overview
Building forms traditionally requires significant boilerplate:
-
Individual state declarations for each field
-
Manual label + input pairing
-
Repetitive styling
-
Custom helper methods
The form abstractions reduce this to concise, declarative code:
// Centralized state for all form fields
FormState form = FormState.builder()
.textField("fullName", "Ada Lovelace")
.textField("email", "ada@example.com")
.booleanField("subscribe", true)
.selectField("country", Arrays.asList("USA", "UK", "Germany"), 0)
.build();
// Simple field declarations
Element overviewExample = formField("Full Name", form.textField("fullName"))
.labelWidth(12)
.rounded()
.borderColor(Color.DARK_GRAY)
.focusedBorderColor(Color.CYAN);
Form Fields
The formField() factory creates a labeled input field that pairs a label with an input widget.
Basic Usage
import static dev.tamboui.toolkit.Toolkit.*;
// With TextInputState
TextInputState nameState = new TextInputState("John");
formField("Name", nameState);
// Creates internal state automatically
formField("Email")
.placeholder("you@example.com");
Field Types
FormFieldElement supports multiple input types:
| Type | Description | State Class |
|---|---|---|
|
Single-line text input |
|
|
Boolean checkbox with [x] rendering |
|
|
On/off toggle switch |
|
|
Dropdown selection |
|
Text Fields
TextInputState emailState = new TextInputState("");
formField("Email", emailState)
.placeholder("you@example.com")
.rounded();
Password Fields
Use maskedField() in the FormState builder for password fields:
FormState form = FormState.builder()
.textField("username", "")
.maskedField("password", "") // automatically masked with '*'
.build();
When used with FormElement, masking is applied automatically.
For formField(), you can use .masked() explicitly:
formField("Password", passwordState)
.placeholder("Enter password")
.masked() // displays '*' for each character
.rounded();
// Or with a custom mask character
formField("PIN", pinState)
.masked('\u25CF'); // displays a filled circle for each character
The actual text is stored in the state; only the display is masked.
Boolean Fields (Checkbox/Toggle)
BooleanFieldState subscribeState = new BooleanFieldState(false);
// Checkbox style: [x] or [ ]
formField("Subscribe", subscribeState);
// Toggle style: [ON] or [OFF]
formField("Dark Mode", subscribeState, FieldType.TOGGLE);
Select Fields
SelectFieldState countryState = new SelectFieldState("USA", "UK", "Germany");
formField("Country", countryState);
Styling
FormFieldElement supports comprehensive styling options:
formField("Email", emailState)
.labelWidth(14) // Fixed label width for alignment
.spacing(2) // Gap between label and input
.rounded() // Rounded border
.borderColor(Color.DARK_GRAY)
.focusedBorderColor(Color.CYAN)
.errorBorderColor(Color.RED);
CSS Selectors
FormFieldElement exposes these CSS child selectors:
| Selector | Description |
|---|---|
|
The label text style |
|
The input wrapper |
|
The inline error message style |
Example TCSS:
FormFieldElement-label {
color: gray;
}
FormFieldElement-error {
color: red;
}
Form State
FormState provides centralized state management for all form fields, eliminating the need for individual state declarations.
Creating Form State
FormState form = FormState.builder()
// Text fields
.textField("username", "")
.textField("email", "user@example.com")
// Boolean fields
.booleanField("newsletter", true)
.booleanField("darkMode", false)
// Select fields
.selectField("country", Arrays.asList("USA", "UK", "Germany"), 0)
.selectField("role", Arrays.asList("Admin", "User", "Guest")) // defaults to index 0
.build();
Accessing Values
// Get state objects for UI binding
TextInputState usernameState = form.textField("username");
BooleanFieldState newsletterState = form.booleanField("newsletter");
SelectFieldState countryState = form.selectField("country");
// Get/set text values directly
String email = form.textValue("email");
form.setTextValue("email", "new@example.com");
// Get/set boolean values
boolean subscribed = form.booleanValue("newsletter");
form.setBooleanValue("newsletter", false);
// Get/set select values
String country = form.selectValue("country");
int countryIndex = form.selectIndex("country");
form.selectIndex("country", 2); // Select "Germany"
// Get all text values as map
Map<String, String> allText = form.textValues();
Validation
The validation framework provides built-in validators and supports custom validation logic.
Built-in Validators
| Validator | Description | Example |
|---|---|---|
|
Field must not be empty |
|
|
Valid email format |
|
|
Minimum n characters |
|
|
Maximum n characters |
|
|
Matches regex pattern |
|
|
Numeric value in range |
|
Using Validators
formField("Email", emailState)
.validate(Validators.required(), Validators.email())
.errorBorderColor(Color.RED)
.showInlineErrors(true);
Custom Error Messages
All built-in validators accept an optional custom message:
Validators.required("Please enter your name");
Validators.email("Invalid email format");
Validators.minLength(3, "Username must be at least 3 characters");
Validators.range(1, 100, "Age must be between 1 and 100");
Triggering Validation
FormFieldElement field = formField("Email", emailState)
.validate(Validators.required(), Validators.email());
// Validate and get result
ValidationResult result = field.validateField();
if (!result.isValid()) {
String error = result.errorMessage(); // "Field is required" or "Invalid email"
}
// Get last validation result
ValidationResult lastResult = field.lastValidation();
Custom Validators
Create custom validators using the Validator functional interface:
Validator usernameAvailable = value -> {
if (userService.exists(value)) {
return ValidationResult.invalid("Username already taken");
}
return ValidationResult.valid();
};
formField("Username", usernameState)
.validate(Validators.required(), usernameAvailable);
Composing Validators
Validators can be composed using and():
Validator emailValidator = Validators.required()
.and(Validators.email())
.and(Validators.maxLength(100));
formField("Email", emailState)
.validate(emailValidator);
Form Container
FormElement provides a container for managing multiple form fields with optional grouping.
Basic Usage
form(formState)
.field("fullName", "Full Name")
.field("email", "Email")
.field("role", "Role")
.labelWidth(14)
.rounded();
Grouping Fields
form(formState)
.group("Personal Info")
.field("fullName", "Full Name")
.field("email", "Email")
.field("phone", "Phone")
.group("Preferences")
.field("newsletter", "Newsletter", FieldType.CHECKBOX)
.field("darkMode", "Dark Mode", FieldType.TOGGLE)
.labelWidth(14)
.spacing(1);
Form Submission
FormElement supports two submission patterns:
Submit on Enter
When enabled, pressing Enter in any text field triggers form submission:
form(formState)
.field("username", "Username")
.field("password", "Password")
.submitOnEnter(true)
.onSubmit(state -> {
String user = state.textValue("username");
String pass = state.textValue("password");
authenticate(user, pass);
});
Programmatic Submit (Button)
Call submit() from a button or other trigger:
FormElement loginForm = form(formState)
.field("username", "Username", Validators.required())
.field("password", "Password", Validators.required())
.onSubmit(state -> authenticate(state));
// Render form with submit button
column(
loginForm,
text(" Login ").bold() // Note: button() is not yet available
);
Validation on Submit
By default, submit() validates all fields before calling the onSubmit callback.
If validation fails, the onSubmit callback is not called and submit() returns false.
|
FormElement form = form(formState)
.field("email", "Email", Validators.required(), Validators.email())
.validateOnSubmit(true) // default behavior
.onSubmit(state -> {
// This is only called if ALL validations pass
save(state);
});
boolean success = form.submit();
// success == true -> validation passed, onSubmit was called
// success == false -> validation failed, onSubmit was NOT called
To always call onSubmit regardless of validation:
form(formState)
.validateOnSubmit(false) // skip validation
.onSubmit(state -> save(state)); // always called
Arrow Key Navigation
By default, Up/Down arrow keys only work in select fields (to change selection).
Enable arrowNavigation to allow Up/Down to move between fields:
form(formState)
.field("username", "Username")
.field("email", "Email")
.field("role", "Role", FieldType.SELECT)
.arrowNavigation(true); // Up/Down navigate between fields
When enabled:
-
Text fields: Up/Down navigate to previous/next field (like Tab/Shift+Tab)
-
Boolean fields (checkbox, toggle): Up/Down navigate to previous/next field
-
Select fields: Up/Down still change the selection (unchanged behavior)
Complete Example
Here’s a complete form example using all the abstractions:
public static class SettingsForm {
private static final FormState FORM = FormState.builder()
// Profile
.textField("fullName", "Ada Lovelace")
.textField("email", "ada@analytical.io")
.textField("role", "Research")
.textField("timezone", "UTC+1")
// Preferences
.textField("theme", "Nord")
.booleanField("notifications", true)
// Security
.textField("twoFa", "Enabled")
.build();
public static Element render() {
return column(
panel("Profile", column(
formField("Full name", FORM.textField("fullName"))
.labelWidth(14).rounded()
.borderColor(Color.DARK_GRAY)
.focusedBorderColor(Color.CYAN),
formField("Email", FORM.textField("email"))
.labelWidth(14).rounded()
.borderColor(Color.DARK_GRAY)
.focusedBorderColor(Color.CYAN)
.validate(Validators.required(), Validators.email()),
formField("Role", FORM.textField("role"))
.labelWidth(14).rounded()
.borderColor(Color.DARK_GRAY)
.focusedBorderColor(Color.CYAN)
).spacing(1)).rounded().borderColor(Color.CYAN),
panel("Preferences", column(
formField("Theme", FORM.textField("theme"))
.labelWidth(14).rounded()
.borderColor(Color.DARK_GRAY),
formField("Notifications", FORM.booleanField("notifications"))
.labelWidth(14)
).spacing(1)).rounded().borderColor(Color.GREEN),
row(
text(" Save ").bold().black().onGreen(),
text(" Cancel ").bold().white().bg(Color.DARK_GRAY)
).spacing(2)
).spacing(1).fill();
}
}
State Classes Reference
TextInputState
TextInputState state = new TextInputState("initial");
// Get/set text
String text = state.text();
state.setText("new value");
// Cursor operations
state.insert('c');
state.deleteBackward();
state.deleteForward();
state.moveCursorLeft();
state.moveCursorRight();
state.clear();
BooleanFieldState
BooleanFieldState state = new BooleanFieldState(false);
// Get/set value
boolean value = state.value();
state.setValue(true);
// Toggle
state.toggle(); // Flips the value
SelectFieldState
SelectFieldState state = new SelectFieldState("A", "B", "C");
// Or with List
SelectFieldState stateFromList = new SelectFieldState(options, 1); // Select index 1
// Get values
String selected = state.selectedValue(); // "A"
int index = state.selectedIndex(); // 0
List<String> opts = state.options(); // ["A", "B", "C"]
// Change selection
state.selectIndex(2); // Select "C"
state.selectNext(); // Move to next option (wraps)
state.selectPrevious(); // Move to previous option (wraps)
Migration Guide
To migrate from manual form construction to the new abstractions:
Before
// Individual state declarations
private static final TextInputState FULL_NAME = new TextInputState("Ada");
private static final TextInputState EMAIL = new TextInputState("ada@example.com");
private static final TextInputState ROLE = new TextInputState("Research");
// Custom helper
private static Element formRow(String label, TextInputState state) {
return row(
text(label).dim().length(14),
textInput(state).rounded().borderColor(Color.DARK_GRAY).fill()
).spacing(1).length(3);
}
// Usage
Element beforeExample1 = formRow("Full name", FULL_NAME);
Element beforeExample2 = formRow("Email", EMAIL);
After
// Centralized state
private static final FormState FORM_STATE = FormState.builder()
.textField("fullName", "Ada")
.textField("email", "ada@example.com")
.textField("role", "Research")
.build();
// Usage - no helper needed
Element afterExample1 = formField("Full name", FORM_STATE.textField("fullName"))
.labelWidth(14).rounded().borderColor(Color.DARK_GRAY);
Element afterExample2 = formField("Email", FORM_STATE.textField("email"))
.labelWidth(14).rounded().borderColor(Color.DARK_GRAY);