Form
This documentation will guide you through creating and managing forms using the Velvet Framework. We'll start with simple concepts and gradually explore more advanced features, providing examples along the way.
Creating a Form
To start building forms with the Velvet Framework, you need to understand the fundamental hooks provided: useInput
and useForm
. These hooks allow you to manage the state of individual input fields and the form as a whole.
Example: Simple Form
Let's start with a basic example. Assume you want to create a login form with two fields: username and password.
typedef UseMyFormReturn = ({
required UseTextInputReturn username,
required UseTextInputReturn password,
required UseFormReturn form,
});
UseMyFormReturn useMyForm() {
final username = useTextInput(
rules: [
RequiredRule(), // A rule ensuring the input is not empty
],
);
final password = useTextInput(
rules: [
RequiredRule(),
MinLengthRule(8), // Ensures password is at least 8 characters long
],
);
final form = useForm(
[username, password], // List of input fields
() async {
// Perform login logic here
},
);
return (
username: username,
password: password,
form: form,
);
}
Implementing in a Widget
class LoginForm extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final (:form, :username, :password) = useMyForm();
return Scaffold(
appBar: AppBar(
title: Text('Login Form'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
onChanged: username.onChanged,
decoration: InputDecoration(
labelText: 'Username',
errorText: username.error.value,
),
),
SizedBox(height: 16),
TextField(
onChanged: password.onChanged,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: password.error.value,
),
),
SizedBox(height: 32),
ElevatedButton(
onPressed: form.submit,
child: form.isSubmitting.value
? CircularProgressIndicator(color: Colors.white)
: Text('Login'),
),
],
),
),
);
}
}
The useInput
Hook
The useInput
hook is a fundamental building block for managing the state and validation of individual form inputs. Wrapping it, it possibile to define another input hooks like useTextInput
, useCheckboxInput
, useSelectInput
, etc.
For example, Velvet provides already a useTextInput
hook that wraps the useInput
hook and provides addionally a TextInputController.
Usage
Here's how you can use the useInput
hook in your form:
final username = useInput<String>(
rules: [
RequiredRule(), // Ensures the field is not empty
MinLengthRule(3), // Ensures the input has at least 3 characters
],
);
This hook returns an object that includes:
- value: The current value of the input.
- error: Any validation error message.
- isValid: A boolean indicating if the current value passes all validation rules.
- focusNode: A focus node to manage focus.
- validate: A function to trigger validation manually.
Options
The useInput
hook accepts several options to customize its behavior:
rules
: A list of validation rules applied to the input.initialValue
: The initial value of the input.name
: A name for the input field, useful for debugging and error handling.options
: AnInputOptions
object that controls behavior like when to validate or clear errors.
Example of using InputOptions
:
final username = useInput<String>(
rules: [
RequiredRule(),
],
options: InputOptions(
shouldValidateOnChange: true, // Validates input on every change
shouldClearErrorOnFocus: true, // Clears error when input gains focus
),
);
The useForm
Hook
The useForm
hook manages the state and behavior of an entire form, including form submission, validation, and error handling.
Usage
Here's an example of how to use the useForm
hook:
final form = useForm(
[username, password], // The inputs managed by the form
() async {
// Your form submission logic, e.g., calling an API
},
onSuccess: () {
// Logic to execute after a successful submission
},
);
The useForm
hook returns:
isSubmitting
: A boolean indicating if the form is currently submitting.isValid
: A boolean indicating if all inputs in the form are valid.submit
: A function to trigger form submission.validate
: A function to manually trigger validation on all inputs.
Options
useForm
also accepts an options
parameter to configure its behavior:
shouldValidateImmediately
: Whether to validate all inputs as soon as the form is initialized.shouldValidateImmediatelyQuietly
: If true, performs an initial validation without showing errors.
Example:
final form = useForm(
[username, password],
() async {
// Form submission logic
},
options: FormOptions(
shouldValidateImmediately: true, // Validates form on initialization
),
);
Custom Input Validations
Creating Custom Rules
In many cases, you’ll need to implement custom validation logic that goes beyond the standard rules. This is where custom rules come into play.
Example of a Custom Rule
Here's an example of creating a custom validation rule that checks if a string contains a specific word:
class ContainsWordRule extends Rule<String> {
final String word;
ContainsWordRule(this.word);
@override
String? validate(String value) {
if (!value.contains(word)) {
return 'The input must contain the word $word.';
}
return null;
}
}
You can then use this custom rule in your useInput
hook:
final customInput = useTextInput(
rules: [ContainsWordRule('flutter')],
);
Handling Errors
Exception Handling with ExceptionMatcher
Forms often need to handle errors that occur during submission or validation. The framework provides the ExceptionMatcher
to manage this.
Configuring ExceptionMatcher
You can configure ExceptionMatcher
to catch specific exceptions and map them to user-friendly messages. For example:
final customInput = useTextInput(
exceptionToMessageResolverFactories: [
() => ExceptionToMessageResolver<FormatException>(
(e) => 'Invalid format.',
),
],
);
This will resolve FormatException
to a specific error message.
Using Default ExceptionMatcher
By default, the framework provides a mechanism to match exceptions to messages using the defaultInputExceptionMatcherFactory
. This can be customized in your FormConfigContract
implementation.
Customizing Form Behavior
FormOptions for Custom Behavior
FormOptions
provides ways to customize form behavior, such as whether to validate immediately or validate quietly:
final form = useForm(
[username, password],
submitForm,
options: FormOptions(
shouldValidateImmediately: false,
shouldValidateImmediatelyQuietly: true,
),
);
shouldValidateImmediately
: Controls if the form should validate all inputs as soon as it's created.shouldValidateImmediatelyQuietly
: When set to true, the validation is done without showing error messages.
Debugging Forms
Precompiled Values
For debugging purposes, you can initialize forms with precompiled values. This can be configured in your FormConfigContract
implementation.
class CustomFormConfig implements FormConfigContract {
@override
Map<String, dynamic> get precompiledValues => {
'username': 'debug_user',
'password': 'debug_pass',
};
}
This allows the form fields to be initialized with these values when debugging.
Handling Exceptions
You can customize how exceptions are handled by overriding the defaultInputExceptionMatcherFactory
method in your FormConfigContract
implementation or by providing a custom implementation of ExceptionMatcher
in the useForm hook.
@override
ExceptionMatcherFactory get defaultInputExceptionMatcherFactory =>
(exceptionToMessageResolverFactories, error) {
return (exception) {
if (kDebugMode) {
print('Exception caught: $exception');
}
// Handle exception as usual
};
};
This will print exceptions to the console in debug mode.
Clearing and Resetting Forms
Clearing Errors on Input Change
You can configure inputs to automatically clear errors when the user starts typing again by setting shouldClearErrorOnChange
in InputOptions
:
final input = useTextInput(
options: InputOptions(shouldClearErrorOnChange: true),
);
Resetting Form State
If you need to reset the form to its initial state, including clearing all inputs and errors, you can implement a reset function:
void resetForm() {
username.value.value = '';
password.value.value = '';
form.validate(quietly: true);
}
This function clears all inputs and re-validates the form quietly, ensuring the form returns to its initial state.