Form State#
duxt_signals provides specialized form signals for managing form state with validation.
FormField#
Create a form field with validators:
final email = formField('', validators: [
required('Email is required'),
email('Invalid email format'),
]);
// Value
print(email()); // ""
// Validation
print(email.isValid); // false
print(email.error); // "Email is required"
// Update
email.set('[email protected]');
print(email.isValid); // true
print(email.error); // null
Built-in Validators#
required#
required('Field is required')
requiredValue#
For nullable types:
requiredValue<int>('Select a value')
minLength / maxLength#
minLength(3, 'At least 3 characters')
maxLength(100, 'Max 100 characters')
email#
email('Invalid email address')
pattern#
pattern(RegExp(r'^\d{5}$'), 'Must be 5 digits')
min / max#
For numbers:
min(1, 'Must be at least 1')
max(100, 'Cannot exceed 100')
notEmpty#
For lists:
notEmpty<int>('Select at least one item')
compose#
Combine validators:
compose([
required(),
minLength(8),
pattern(RegExp(r'[A-Z]'), 'Must have uppercase'),
])
Field State#
FormField tracks additional state beyond the value:
final name = formField('');
// touched - has the field been focused/blurred?
print(name.touched); // false
name.touch();
print(name.touched); // true
// dirty - has the value changed from initial?
print(name.dirty); // false
name.set('John');
print(name.dirty); // true
// error - current validation error
print(name.error); // null or error message
// isValid - is the field valid?
print(name.isValid); // true/false
Resetting Fields#
Reset to initial state:
final name = formField('initial');
name.set('changed');
name.touch();
name.reset();
// name() == 'initial'
// name.touched == false
// name.dirty == false
FormState#
Group multiple fields together:
final nameField = formField('', validators: [required()]);
final emailField = formField('', validators: [email()]);
final ageField = formField(18, validators: [min(18)]);
final form = FormState({
'name': nameField,
'email': emailField,
'age': ageField,
});
// All values
print(form.values);
// {'name': '', 'email': '', 'age': 18}
// All errors
print(form.errors);
// {'name': 'This field is required'}
// Form validity
print(form.isValid); // false
Form Methods#
validate#
Validate all fields:
if (form.validate()) {
print('Form is valid!');
}
touchAll#
Mark all fields as touched:
form.touchAll();
reset#
Reset all fields:
form.reset();
setValues#
Set multiple values:
form.setValues({
'name': 'John Doe',
'email': '[email protected]',
});
submit#
Submit if valid:
form.submit((data) async {
await api.createUser(data);
});
Using with DuxtUI#
DInput(
label: 'Email',
value: emailField(),
onInput: emailField.set,
error: emailField.touched ? emailField.error : null,
onBlur: (_) => emailField.touch(),
),
Complete Form Example#
// Define fields
final titleField = formField('', validators: [
required('Title is required'),
minLength(3, 'At least 3 characters'),
]);
final contentField = formField('', validators: [
required('Content is required'),
]);
final publishedField = formField(false);
final categoryField = formField<int?>(null, validators: [
requiredValue('Select a category'),
]);
// Create form state
final form = FormState({
'title': titleField,
'content': contentField,
'published': publishedField,
'categoryId': categoryField,
});
// Build form UI
Column([
DInput(
label: 'Title',
value: titleField(),
onInput: titleField.set,
error: titleField.error,
),
DTextarea(
label: 'Content',
value: contentField(),
onInput: contentField.set,
error: contentField.error,
),
DSwitch(
label: 'Published',
checked: publishedField(),
onChange: publishedField.set,
),
DSelect<int>(
label: 'Category',
value: categoryField(),
onChange: categoryField.set,
options: categories.map((c) =>
DSelectOption(value: c.id, label: c.name)
).toList(),
),
DButton(
label: form.isValid ? 'Save' : 'Fill required fields',
disabled: !form.isValid,
onClick: () => form.submit((data) async {
await Api.post('/posts', body: data);
}),
),
]),