Forms
Introduction
Nylo Website v7 provides a form system built around NyFormWidget. Your form class extends NyFormWidget and is the widget — no separate wrapper needed. Forms support built-in validation, many field types, styling, and data management.
import 'package:nylo_framework/nylo_framework.dart';
// 1. Define a form
class LoginForm extends NyFormWidget {
LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});
@override
fields() => [
Field.email("Email", validator: FormValidator.email()),
Field.password("Password", validator: FormValidator.password()),
];
static NyFormActions get actions => const NyFormActions('LoginForm');
}
// 2. Display and submit it
LoginForm(
submitButton: Button.primary(text: "Login"),
onSubmit: (data) {
print(data); // {email: "...", password: "..."}
},
)
Creating a Form
Use the Metro CLI to create a new form:
metro make:form LoginForm
This creates lib/app/forms/login_form.dart:
import 'package:nylo_framework/nylo_framework.dart';
class LoginForm extends NyFormWidget {
LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});
@override
fields() => [
Field.email("Email", validator: FormValidator.email()),
Field.password("Password", validator: FormValidator.password()),
];
static NyFormActions get actions => const NyFormActions('LoginForm');
}
Forms extend NyFormWidget and override the fields() method to define form fields. Each field uses a named constructor like Field.text(), Field.email(), or Field.password(). The static NyFormActions get actions getter provides a convenient way to interact with the form from anywhere in your app.
Displaying a Form
Since your form class extends NyFormWidget, it is the widget. Use it directly in your widget tree:
@override
Widget view(BuildContext context) {
return Scaffold(
body: SafeArea(
child: LoginForm(
submitButton: Button.primary(text: "Submit"),
onSubmit: (data) {
print(data);
},
),
),
);
}
Submitting a Form
There are three ways to submit a form:
Using onSubmit and submitButton
Pass onSubmit and a submitButton when constructing the form. Nylo Website provides pre-built buttons that work as submit buttons:
LoginForm(
submitButton: Button.primary(text: "Submit"),
onSubmit: (data) {
print(data); // {email: "...", password: "..."}
},
onFailure: (errors) {
print(errors.first.rule.getMessage());
},
)
Available button styles: Button.primary, Button.secondary, Button.outlined, Button.textOnly, Button.icon, Button.gradient, Button.rounded, Button.transparency.
Using NyFormActions
Use the actions getter to submit from anywhere:
LoginForm.actions.submit(
onSuccess: (data) {
print(data);
},
onFailure: (errors) {
print(errors.first.rule.getMessage());
},
showToastError: true,
);
Using NyFormWidget.submit() static method
Submit a form by its name from anywhere:
NyFormWidget.submit("LoginForm",
onSuccess: (data) {
print(data);
},
onFailure: (errors) {
print(errors.first.rule.getMessage());
},
showToastError: true,
);
When submitted, the form validates all fields. If valid, onSuccess is called with a Map<String, dynamic> of field data (keys are snake_case versions of field names). If invalid, a toast error is shown by default and onFailure is called if provided.
Field Types
Nylo Website v7 provides 22 field types via named constructors on the Field class. All field constructors share these common parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
key |
String |
Required | The field identifier (positional) |
label |
String? |
null |
Custom display label (defaults to key in title case) |
value |
dynamic |
null |
Initial value |
validator |
FormValidator? |
null |
Validation rules |
autofocus |
bool |
false |
Auto-focus on load |
dummyData |
String? |
null |
Test/development data |
header |
Widget? |
null |
Widget displayed above the field |
footer |
Widget? |
null |
Widget displayed below the field |
titleStyle |
TextStyle? |
null |
Custom label text style |
hidden |
bool |
false |
Hide the field |
readOnly |
bool? |
null |
Make field read-only |
style |
FieldStyle? |
Varies | Field-specific style configuration |
onChanged |
Function(dynamic)? |
null |
Value change callback |
Text Fields
Field.text("Name")
Field.text("Name",
value: "John",
validator: FormValidator.notEmpty(),
autofocus: true,
)
Style type: FieldStyleTextField
Number Fields
Field.number("Age")
// Decimal numbers
Field.number("Score", decimal: true)
The decimal parameter controls whether decimal input is allowed. Style type: FieldStyleTextField
Password Fields
Field.password("Password")
// With visibility toggle
Field.password("Password", viewable: true)
The viewable parameter adds a show/hide toggle. Style type: FieldStyleTextField
Email Fields
Field.email("Email", validator: FormValidator.email())
Automatically sets the email keyboard type and filters whitespace. Style type: FieldStyleTextField
URL Fields
Field.url("Website", validator: FormValidator.url())
Sets URL keyboard type. Style type: FieldStyleTextField
Text Area Fields
Field.textArea("Description")
Multi-line text input. Style type: FieldStyleTextField
Phone Number Fields
Field.phoneNumber("Mobile Phone")
Automatically formats phone number input. Style type: FieldStyleTextField
Capitalize Words
Field.capitalizeWords("Full Name")
Capitalizes the first letter of each word. Style type: FieldStyleTextField
Capitalize Sentences
Field.capitalizeSentences("Bio")
Capitalizes the first letter of each sentence. Style type: FieldStyleTextField
Date Fields
Field.date("Birthday")
Field.date("Birthday",
dummyData: "1990-01-01",
style: FieldStyleDateTimePicker(
firstDate: DateTime(1900),
lastDate: DateTime.now(),
),
)
Opens a date picker. Style type: FieldStyleDateTimePicker
DateTime Fields
Field.datetime("Check in Date")
Field.datetime("Appointment", dummyData: "2025-01-01 10:00")
Opens a date and time picker. Style type: FieldStyleDateTimePicker
Masked Input Fields
Field.mask("Phone", mask: "(###) ###-####")
Field.mask("Credit Card", mask: "#### #### #### ####")
Field.mask("Custom Code",
mask: "AA-####",
match: r'[\w\d]',
maskReturnValue: true, // Returns the formatted value
)
The # character in the mask is replaced by user input. Use match to control the allowed characters. When maskReturnValue is true, the returned value includes the mask formatting.
Currency Fields
Field.currency("Price", currency: "usd")
The currency parameter is required and determines the currency format. Style type: FieldStyleTextField
Checkbox Fields
Field.checkbox("Accept Terms")
Field.checkbox("Agree to terms",
header: Text("Terms and conditions"),
footer: Text("You must agree to continue."),
validator: FormValidator.booleanTrue(message: "You must accept the terms"),
)
Style type: FieldStyleCheckbox
Switch Box Fields
Field.switchBox("Enable Notifications")
Style type: FieldStyleSwitchBox
Picker Fields
Field.picker("Category",
options: FormCollection.from(["Electronics", "Clothing", "Books"]),
)
// With key-value pairs
Field.picker("Country",
options: FormCollection.fromMap({
"us": "United States",
"ca": "Canada",
"uk": "United Kingdom",
}),
)
The options parameter requires a FormCollection (not a raw list). See FormCollection for details. Style type: FieldStylePicker
Radio Fields
Field.radio("Newsletter",
options: FormCollection.fromMap({
"Yes": "Yes, I want to receive newsletters",
"No": "No, I do not want to receive newsletters",
}),
)
The options parameter requires a FormCollection. Style type: FieldStyleRadio
Chip Fields
Field.chips("Tags",
options: FormCollection.from(["Featured", "Sale", "New"]),
)
// With key-value pairs
Field.chips("Engine Size",
options: FormCollection.fromMap({
"125": "125cc",
"250": "250cc",
"500": "500cc",
}),
)
Allows multi-selection via chip widgets. The options parameter requires a FormCollection. Style type: FieldStyleChip
Slider Fields
Field.slider("Rating",
label: "Rate us",
validator: FormValidator.minValue(4, message: "Rating must be at least 4"),
style: FieldStyleSlider(
min: 0,
max: 10,
divisions: 10,
activeColor: Colors.blue,
inactiveColor: Colors.grey,
),
)
Style type: FieldStyleSlider — configure min, max, divisions, colors, value display, and more.
Range Slider Fields
Field.rangeSlider("Price Range",
style: FieldStyleRangeSlider(
min: 0,
max: 1000,
divisions: 20,
activeColor: Colors.blue,
inactiveColor: Colors.grey,
),
)
Returns a RangeValues object. Style type: FieldStyleRangeSlider
Custom Fields
Use Field.custom() to provide your own stateful widget:
Field.custom("My Field",
child: MyCustomFieldWidget(),
)
The child parameter requires a widget that extends NyFieldStatefulWidget. This gives you full control over the field's rendering and behavior.
Widget Fields
Use Field.widget() to embed any widget inside the form without it being a form field:
Field.widget(child: Divider())
Field.widget(child: Text("Section Header", style: TextStyle(fontSize: 18)))
Widget fields don't participate in validation or data collection. They are purely for layout.
FormCollection
Picker, radio, and chip fields require a FormCollection for their options. FormCollection provides a unified interface for handling different option formats.
Creating a FormCollection
// From a list of strings (value and label are the same)
FormCollection.from(["Red", "Green", "Blue"])
// Same as above, explicit
FormCollection.fromArray(["Red", "Green", "Blue"])
// From a map (key = value, value = label)
FormCollection.fromMap({
"us": "United States",
"ca": "Canada",
})
// From structured data (useful for API responses)
FormCollection.fromKeyValue([
{"value": "en", "label": "English"},
{"value": "es", "label": "Spanish"},
])
FormCollection.from() auto-detects the data format and delegates to the appropriate constructor.
FormOption
Each option in a FormCollection is a FormOption with value and label properties:
FormOption option = FormOption(value: "us", label: "United States");
print(option.value); // "us"
print(option.label); // "United States"
Querying Options
FormCollection options = FormCollection.fromMap({"us": "United States", "ca": "Canada"});
options.getByValue("us"); // FormOption(value: us, label: United States)
options.getLabelByValue("us"); // "United States"
options.containsValue("ca"); // true
options.searchByLabel("can"); // [FormOption(value: ca, label: Canada)]
options.values; // ["us", "ca"]
options.labels; // ["United States", "Canada"]
Form Validation
Add validation to any field using the validator parameter with FormValidator:
// Named constructor
Field.email("Email", validator: FormValidator.email())
// Chained rules
Field.text("Username",
validator: FormValidator()
.notEmpty()
.minLength(3)
.maxLength(20)
)
// Password with strength level
Field.password("Password",
validator: FormValidator.password(strength: 2)
)
// Boolean validation
Field.checkbox("Terms",
validator: FormValidator.booleanTrue(message: "You must accept the terms")
)
// Custom inline validation
Field.number("Age",
validator: FormValidator.custom(
message: "Age must be between 18 and 100",
validate: (data) {
int? age = int.tryParse(data.toString());
return age != null && age >= 18 && age <= 100;
},
)
)
When a form is submitted, all validators are checked. If any fail, a toast error shows the first error message and the onFailure callback is called.
See also: Validation for a full list of available validators.
Managing Form Data
Initial Data
There are two ways to set initial data on a form.
Option 1: Override the init getter in your form class
class EditAccountForm extends NyFormWidget {
EditAccountForm({super.key, super.submitButton, super.onSubmit, super.onFailure});
@override
Function()? get init => () async {
final user = await api<ApiService>((request) => request.getUserData());
return {
"First Name": user?.firstName,
"Last Name": user?.lastName,
};
};
@override
fields() => [
Field.text("First Name"),
Field.text("Last Name"),
];
static NyFormActions get actions => const NyFormActions('EditAccountForm');
}
The init getter can return either a synchronous Map or an async Future<Map>. Keys are matched to field names using snake_case normalization, so "First Name" maps to a field with key "First Name".
Option 2: Pass initialData to the form widget
EditAccountForm(
initialData: {
"first_name": "John",
"last_name": "Doe",
},
)
Setting Field Values
Use NyFormActions to set field values from anywhere:
// Set a single field value
EditAccountForm.actions.updateField("First Name", "Jane");
Setting Field Options
Update options on picker, chip, or radio fields dynamically:
EditAccountForm.actions.setOptions("Category", FormCollection.from(["New Option 1", "New Option 2"]));
Reading Form Data
Form data is accessed through the onSubmit callback when the form is submitted, or through the onChanged callback for real-time updates:
EditAccountForm(
onSubmit: (data) {
// data is a Map<String, dynamic>
// {first_name: "Jane", last_name: "Doe", email: "jane@example.com"}
print(data);
},
onChanged: (Field field, dynamic value) {
print("${field.key} changed to: $value");
},
)
Clearing Data
// Clear all fields
EditAccountForm.actions.clear();
// Clear a specific field
EditAccountForm.actions.clearField("First Name");
Updating Fields
// Update a field value
EditAccountForm.actions.updateField("First Name", "Jane");
// Refresh the form UI
EditAccountForm.actions.refresh();
// Refresh form fields (re-calls fields())
EditAccountForm.actions.refreshForm();
Submit Button
Pass a submitButton and onSubmit callback when constructing the form:
UserInfoForm(
submitButton: Button.primary(text: "Submit"),
onSubmit: (data) {
print(data);
},
onFailure: (errors) {
print(errors.first.rule.getMessage());
},
)
The submitButton is automatically displayed below the form fields. You can use any of the built-in button styles or a custom widget.
You can also use any widget as a submit button by passing it as a footer:
UserInfoForm(
onSubmit: (data) {
print(data);
},
footer: ElevatedButton(
onPressed: () {
UserInfoForm.actions.submit(
onSuccess: (data) {
print(data);
},
);
},
child: Text("Submit"),
),
)
Form Layout
Place fields side-by-side by wrapping them in a List:
@override
fields() => [
// Single field (full width)
Field.text("Title"),
// Two fields in a row
[
Field.text("First Name"),
Field.text("Last Name"),
],
// Another single field
Field.textArea("Bio"),
// Slider and range slider in a row
[
Field.slider("Rating", style: FieldStyleSlider(min: 0, max: 10)),
Field.rangeSlider("Budget", style: FieldStyleRangeSlider(min: 0, max: 1000)),
],
// Embed a non-field widget
Field.widget(child: Divider()),
Field.email("Email"),
];
Fields in a List are rendered in a Row with equal Expanded widths. The spacing between fields is controlled by the crossAxisSpacing parameter on NyFormWidget.
Field Visibility
Show or hide fields programmatically using the hide() and show() methods on Field. You can access fields inside your form class or through the onChanged callback:
// Inside your NyFormWidget subclass or onChanged callback
Field nameField = ...;
// Hide the field
nameField.hide();
// Show the field
nameField.show();
Hidden fields are not rendered in the UI but still exist in the form's field list.
Field Styling
Each field type has a corresponding FieldStyle subclass for styling:
| Field Type | Style Class |
|---|---|
| Text, Email, Password, Number, URL, TextArea, PhoneNumber, Currency, Mask, CapitalizeWords, CapitalizeSentences | FieldStyleTextField |
| Date, DateTime | FieldStyleDateTimePicker |
| Picker | FieldStylePicker |
| Checkbox | FieldStyleCheckbox |
| Switch Box | FieldStyleSwitchBox |
| Radio | FieldStyleRadio |
| Chip | FieldStyleChip |
| Slider | FieldStyleSlider |
| Range Slider | FieldStyleRangeSlider |
Pass a style object to the style parameter of any field:
Field.text("Name",
style: FieldStyleTextField(
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
prefixIcon: Icon(Icons.person),
),
)
Field.slider("Rating",
style: FieldStyleSlider(
min: 0,
max: 10,
divisions: 10,
activeColor: Colors.blue,
showValue: true,
),
)
Field.chips("Tags",
options: FormCollection.from(["Sale", "New", "Featured"]),
style: FieldStyleChip(
selectedColor: Colors.blue,
checkmarkColor: Colors.white,
spacing: 8.0,
runSpacing: 8.0,
),
)
NyFormWidget Static Methods
NyFormWidget provides static methods to interact with forms by name from anywhere in your app:
| Method | Description |
|---|---|
NyFormWidget.submit(name, onSuccess:, onFailure:, showToastError:) |
Submit a form by its name |
NyFormWidget.stateRefresh(name) |
Refresh the form's UI state |
NyFormWidget.stateSetValue(name, key, value) |
Set a field value by form name |
NyFormWidget.stateSetOptions(name, key, options) |
Set field options by form name |
NyFormWidget.stateClearData(name) |
Clear all fields by form name |
NyFormWidget.stateRefreshForm(name) |
Refresh form fields (re-calls fields()) |
// Submit a form named "LoginForm" from anywhere
NyFormWidget.submit("LoginForm", onSuccess: (data) {
print(data);
});
// Update a field value remotely
NyFormWidget.stateSetValue("LoginForm", "Email", "new@email.com");
// Clear all form data
NyFormWidget.stateClearData("LoginForm");
Tip: Prefer using
NyFormActions(see below) instead of calling these static methods directly — it's more concise and less error-prone.
NyFormWidget Constructor Reference
When extending NyFormWidget, these are the constructor parameters you can pass:
LoginForm(
Key? key,
double crossAxisSpacing = 10, // Horizontal spacing between row fields
double mainAxisSpacing = 10, // Vertical spacing between fields
Map<String, dynamic>? initialData, // Initial field values
Function(Field field, dynamic value)? onChanged, // Field change callback
Widget? header, // Widget above the form
Widget? submitButton, // Submit button widget
Widget? footer, // Widget below the form
double headerSpacing = 10, // Spacing after header
double submitButtonSpacing = 10, // Spacing after submit button
double footerSpacing = 10, // Spacing before footer
LoadingStyle? loadingStyle, // Loading indicator style
bool locked = false, // Makes form read-only
Function(dynamic data)? onSubmit, // Called with form data on successful validation
Function(dynamic error)? onFailure, // Called with errors on failed validation
)
The onChanged callback receives the Field that changed and its new value:
LoginForm(
onChanged: (Field field, dynamic value) {
print("${field.key} changed to: $value");
},
)
NyFormActions
NyFormActions provides a convenient way to interact with a form from anywhere in your app. Define it as a static getter on your form class:
class LoginForm extends NyFormWidget {
LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});
@override
fields() => [
Field.email("Email", validator: FormValidator.email()),
Field.password("Password", validator: FormValidator.password()),
];
static NyFormActions get actions => const NyFormActions('LoginForm');
}
Available Actions
| Method | Description |
|---|---|
actions.updateField(key, value) |
Set a field's value |
actions.clearField(key) |
Clear a specific field |
actions.clear() |
Clear all fields |
actions.refresh() |
Refresh the form's UI state |
actions.refreshForm() |
Re-call fields() and rebuild |
actions.setOptions(key, options) |
Set options on picker/chip/radio fields |
actions.submit(onSuccess:, onFailure:, showToastError:) |
Submit with validation |
// Update a field value
LoginForm.actions.updateField("Email", "new@email.com");
// Clear all form data
LoginForm.actions.clear();
// Submit the form
LoginForm.actions.submit(
onSuccess: (data) {
print(data);
},
);
NyFormWidget Overrides
Methods you can override in your NyFormWidget subclass:
| Override | Description |
|---|---|
fields() |
Define the form fields (required) |
init |
Provide initial data (sync or async) |
onChange(field, data) |
Handle field changes internally |
All Field Types Reference
| Constructor | Key Parameters | Description |
|---|---|---|
Field.text() |
— | Standard text input |
Field.email() |
— | Email input with keyboard type |
Field.password() |
viewable |
Password with optional visibility toggle |
Field.number() |
decimal |
Numeric input, optional decimal |
Field.currency() |
currency (required) |
Currency-formatted input |
Field.capitalizeWords() |
— | Title case text input |
Field.capitalizeSentences() |
— | Sentence case text input |
Field.textArea() |
— | Multi-line text input |
Field.phoneNumber() |
— | Auto-formatted phone number |
Field.url() |
— | URL input with keyboard type |
Field.mask() |
mask (required), match, maskReturnValue |
Masked text input |
Field.date() |
— | Date picker |
Field.datetime() |
— | Date and time picker |
Field.checkbox() |
— | Boolean checkbox |
Field.switchBox() |
— | Boolean toggle switch |
Field.picker() |
options (required FormCollection) |
Single selection from list |
Field.radio() |
options (required FormCollection) |
Radio button group |
Field.chips() |
options (required FormCollection) |
Multi-select chips |
Field.slider() |
— | Single value slider |
Field.rangeSlider() |
— | Range value slider |
Field.custom() |
child (required NyFieldStatefulWidget) |
Custom stateful widget |
Field.widget() |
child (required Widget) |
Embed any widget (non-field) |