reactive_forms

Last updated:

0 purchases

reactive_forms Image
reactive_forms Images
Add to Cart

Description:

reactive forms

Reactive Forms #
This is a model-driven approach to handling Forms inputs and validations, heavily inspired in Angular's Reactive Forms.

Table of Contents #

Getting Started

Minimum Requirements
Installation and Usage


Creating a form
How to get/set Form data
Validators

Predefined validators
Custom Validators
Pattern Validator
FormGroup validators
Password and Password Confirmation
Asynchronous Validators
Debounce time in async validators
Composing Validators


Groups of Groups
Dynamic forms with FormArray
Arrays of Groups
FormBuilder

Groups
Arrays
Control
Control state


Reactive Form Widgets
How to customize error messages?

Reactive Widget level
Global/Application level
Parameterized validation messages


When does Validation Messages begin to show up?

Touching a control
Overriding Reactive Widgets show errors behavior


Enable/Disable Submit button

Submit Button in a different Widget
ReactiveFormConsumer widget


Focus/UnFocus a FormControl
Focus flow between Text Fields
Enable/Disable a widget
How does ReactiveTextField differs from native TextFormField or TextField?
Reactive Form Field Widgets
Bonus Field Widgets
Other Reactive Forms Widgets
Advanced Reactive Field Widgets
ReactiveValueListenableBuilder to listen when value changes in a FormControl
ReactiveForm vs ReactiveFormBuilder which one?
Reactive Forms + Provider plugin
Reactive Forms + code generation plugin
How create a custom Reactive Widget?
What is not Reactive Forms
What is Reactive Forms
Migrate versions

Getting Started #
For help getting started with Flutter, view the
online documentation, which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Minimum Requirements #

Dart SDK: >=3.2.0 <4.0.0
Flutter: >=3.16.0


For using Reactive Forms in projects below Flutter 2.8.0 please use the version <= 10.7.0 of
Reactive Forms.


For using Reactive Forms in projects below Flutter 2.2.0 please use the version <= 10.2.0 of
Reactive Forms.


For using Reactive Forms in projects with Flutter 1.17.0 please use the version 7.6.3 of
Reactive Forms.


Reactive Forms v8.x includes the intl package. If a version conflict is present, then you should use dependency_overrides to temporarily override all references to intl and set the one that better fits your needs.

Installation and Usage #
Once you're familiar with Flutter you may install this package adding reactive_forms to the dependencies list
of the pubspec.yaml file as follow:
dependencies:
flutter:
sdk: flutter

reactive_forms: ^17.0.1
copied to clipboard
Then run the command flutter packages get on the console.
Creating a form #
A form is composed by multiple fields or controls.
To declare a form with the fields name and email is as simple as:
final form = FormGroup({
'name': FormControl<String>(value: 'John Doe'),
'email': FormControl<String>(),
});
copied to clipboard
Default Values #
Notice in the example above that in the case of the name we have also set a default value, in the case of the email the default value is null.
How to get/set Form data #
Given the FormGroup:
final form = FormGroup({
'name': FormControl<String>(value: 'John Doe'),
'email': FormControl<String>(value: '[email protected]'),
});
copied to clipboard
You can get the value of a single FormControl as simple as:
String get name() => this.form.control('name').value;
copied to clipboard
But you can also get the complete Form data as follows:
print(form.value);
copied to clipboard
The previous code prints the following output:
{
"name": "John Doe",
"email": "[email protected]"
}
copied to clipboard

FormGroup.value returns an instance of Map<String, dynamic> with each field and its value.

To set value to controls you can use two approaches:
// set value directly to the control
this.form.control('name').value = 'John';

// set value to controls by setting value to the form
this.form.value = {
'name': 'John',
'email': '[email protected]',
};
copied to clipboard
What about Validators? #
You can add validators to a FormControl as follows:
final form = FormGroup({
'name': FormControl<String>(validators: [Validators.required]),
'email': FormControl<String>(validators: [
Validators.required,
Validators.email,
]),
});
copied to clipboard

If at least one FormControl is invalid then the FormGroup is invalid

There are common predefined validators, but you can implement custom validators too.
Predefined validators #
FormControl

Validators.required
Validators.requiredTrue
Validators.email
Validators.number
Validators.min
Validators.max
Validators.minLength
Validators.maxLength
Validators.pattern
Validators.creditCard
Validators.equals
Validators.compose
Validators.composeOR
Validators.any
Validators.contains

FormGroup

Validators.mustMatch
Validators.compare

FormArray

Validators.minLength
Validators.maxLength
Validators.any
Validators.contains

Custom Validators #
All validators are instances of classes that inherit from the Validator abstract class.
In order to implement a custom validator you can follow two different approaches:
1- Extend from Validator class and override the validate method.
2- Or implement a custom validator function|method, and use it with the Validators.delegate(...) validator.
Let's implement a custom validator that validates a control's value must be true:
Inheriting from Validator class: #
Let's create a class that extends from Validator and overrides the validate method:
/// Validator that validates the control's value must be `true`.
class RequiredTrueValidator extends Validator<dynamic> {
const RequiredTrueValidator() : super();

@override
Map<String, dynamic>? validate(AbstractControl<dynamic> control) {
return control.isNotNull &&
control.value is bool &&
control.value == true
? null
: {'requiredTrue': true};
}
}
copied to clipboard
The validator method is a function that receives the control to validate and returns a Map. If the value of the control is valid the function returns null, otherwise returns a Map with the error key and a custom information. In the previous example we have defined requiredTrue as the error key and true as the custom information.
In order to use the new validator class we provide an instance of it in the FormControl definition.
final form = FormGroup({
'acceptLicense': FormControl<bool>(
value: false,
validators: [
RequiredTrueValidator(), // providing the new custom validator
],
),
});
copied to clipboard
Using the Validators.delegate() validator: #
Sometimes it's more convenient to implement a custom validator in a separate method|function than in a different new class. In that case, it is necessary to use the Validators.delegate() validator. It creates a validator that delegates the validation to the external function|method.
final form = FormGroup({
'acceptLicense': FormControl<bool>(
value: false,
validators: [
Validators.delegate(_requiredTrue) // delegates validation to a custom function
],
),
});
copied to clipboard
/// Custom function that validates that control's value must be `true`.
Map<String, dynamic>? _requiredTrue(AbstractControl<dynamic> control) {
return control.isNotNull &&
control.value is bool &&
control.value == true
? null
: {'requiredTrue': true};
}
copied to clipboard

Check the Migration Guide to learn more about custom validators after version 15.0.0 of the package.

Pattern Validator #
Validator.pattern is a validator that comes with Reactive Forms. Validation using regular expressions have been always a very useful tool to solve validation requirements. Let's see how we can validate American Express card numbers:

American Express card numbers start with 34 or 37 and have 15 digits.

const americanExpressCardPattern = r'^3[47][0-9]{13}$';

final cardNumber = FormControl<String>(
validators: [Validators.pattern(americanExpressCardPattern)],
);

cardNumber.value = '395465465421'; // not a valid number

expect(cardNumber.valid, false);
expect(cardNumber.hasError('pattern'), true);
copied to clipboard

The above code is a Unit Test extracted from Reactive Forms tests.

If we print the value of FormControl.errors:
print(cardNumber.errors);
copied to clipboard
We will get a Map like this:
{
"pattern": {
"requiredPattern": "^3[47][0-9]{13}$",
"actualValue": 395465465421
}
}
copied to clipboard
FormGroup validators #
There are special validators that can be attached to FormGroup. In the next section we will see an example of that.
What about Password and Password Confirmation? #
There are some cases where we want to implement a Form where a validation of a field depends on the value of another field. For example a sign-up form with email and emailConfirmation or password and passwordConfirmation.
For those cases we could implement a custom validator as a class and attach it to the FormGroup. Let's see an example:
final form = FormGroup({
'name': FormControl<String>(validators: [Validators.required]),
'email': FormControl<String>(validators: [Validators.required, Validators.email]),
'password': FormControl<String>(validators: [
Validators.required,
Validators.minLength(8),
]),
'passwordConfirmation': FormControl<String>(),
}, validators: [
MustMatchValidator(controlName: 'password', matchingControlName: 'passwordConfirmation')
]);
copied to clipboard

Notice the use of *Validators.minLength(8)*

In the previous code we have added two more fields to the form: password and passwordConfirmation, both fields are required and the password must be at least 8 characters length.
However the most important thing here is that we have attached a validator to the FormGroup. This validator is a custom validator and the implementation follows as:
class MustMatchValidator extends Validator<dynamic> {
final String controlName;
final String matchingControlName;

MustMatchValidator({
required this.controlName,
required this.matchingControlName,
}) : super();

@override
Map<String, dynamic>? validate(AbstractControl<dynamic> control) {
final form = control as FormGroup;

final formControl = form.control(controlName);
final matchingFormControl = form.control(matchingControlName);

if (formControl.value != matchingFormControl.value) {
matchingFormControl.setErrors({'mustMatch': true});

// force messages to show up as soon as possible
matchingFormControl.markAsTouched();
} else {
matchingFormControl.removeError('mustMatch');
}

return null;
}
}
copied to clipboard
Fortunately you don't have to implement a custom must match validator because we have already included it into the code of the reactive_forms package so you should reuse it. The previous form definition becomes into:
final form = FormGroup({
'name': FormControl<String>(validators: [Validators.required]),
'email': FormControl<String>(validators: [Validators.required, Validators.email]),
'emailConfirmation': FormControl<String>(),
'password': FormControl<String>(validators: [Validators.required, Validators.minLength(8)]),
'passwordConfirmation': FormControl<String>(),
}, validators: [
Validators.mustMatch('email', 'emailConfirmation'),
Validators.mustMatch('password', 'passwordConfirmation'),
]);
copied to clipboard
Asynchronous Validators 😎 #
Some times you want to perform a validation against a remote server, this operations are more time consuming and need to be done asynchronously.
For example you want to validate that the email the user is currently typing in a registration form is unique and is not already used in your application. Asynchronous Validators are just another tool so use them wisely.
Asynchronous Validators are very similar to their synchronous counterparts, with the following difference:

The validator function returns a Future

Asynchronous validation executes after the synchronous validation, and is performed only if the synchronous validation is successful. This check allows forms to avoid potentially expensive async validation processes (such as an HTTP request) if the more basic validation methods have already found invalid input.
After asynchronous validation begins, the form control enters a pending state. You can inspect the control's pending property and use it to give visual feedback about the ongoing validation operation.
Code speaks more than a thousand words :) so let's see an example.
Let's implement the previous mentioned example: the user is typing the email in a registration Form and you want to validate that the email is unique in your System. We will implement a custom async validator for that purpose.
final form = FormGroup({
'email': FormControl<String>(
validators: [
Validators.required, // traditional required and email validators
Validators.email,
],
asyncValidators: [
UniqueEmailAsyncValidator(), // custom asynchronous validator :)
],
),
});
copied to clipboard
We have declared a simple Form with an email field that is required and must have a valid email value, and we have include a custom async validator that will validate if the email is unique. Let's see the implementation of our new async validator:
/// Validator that validates the user's email is unique, sending a request to
/// the Server.
class UniqueEmailAsyncValidator extends AsyncValidator<dynamic> {
@override
Future<Map<String, dynamic>?> validate(AbstractControl<dynamic> control) async {
final error = {'unique': false};

final isUniqueEmail = await _getIsUniqueEmail(control.value.toString());
if (!isUniqueEmail) {
control.markAsTouched();
return error;
}

return null;
}

/// Simulates a time consuming operation (i.e. a Server request)
Future<bool> _getIsUniqueEmail(String email) {
// simple array that simulates emails stored in the Server DB.
final storedEmails = ['[email protected]', '[email protected]'];

return Future.delayed(
const Duration(seconds: 5),
() => !storedEmails.contains(email),
);
}
}
copied to clipboard

Note the use of control.markAsTouched() to force the validation message to show up as soon as possible.

The previous implementation was a simple validator that receives the AbstractControl and returns a Future that completes 5 seconds after its call and performs a simple check: if the value of the control is contained in the server array of emails.

If you want to see Async Validators in action with a full example using widgets and animations to feedback the user we strong advice you to visit our Wiki. We have not included the full example in this README.md file just to simplify things here and to not anticipate things that we will see later in this doc.


The validator Validators.delegateAsync() is another way to implement custom validator, for more reference
check the Custom validators section.

Debounce time in async validators #
Asynchronous validators have a debounce time that is useful if you want to minimize requests to a remote API. The debounce time is set in milliseconds and the default value is 250 milliseconds.
You can set a different debounce time as an optionally argument in the FormControl constructor.
final control = FormControl<String>(
asyncValidators: [UniqueEmailAsyncValidator()],
asyncValidatorsDebounceTime: 1000, // sets 1 second of debounce time.
);
copied to clipboard
Composing Validators #
To explain what Composing Validators is, let's see an example:
We want to validate a text field of an authentication form.
In this text field the user can write an email or a phone number and we want to make sure that the information is correctly formatted. We must validate that input is a valid email or a valid phone number.

final phonePattern = '<some phone regex pattern>';

final form = FormGroup({
'user': FormControl<String>(
validators: [
Validators.composeOR([
Validators.email,
Validators.pattern(phonePattern),
])
],
),
});
copied to clipboard

Note that Validators.composeOR receives a collection of validators as argument and returns a validator.

With Validators.composeOR we are saying to FormControl that if at least one validator evaluate as VALID then the control is VALID it's not necessary that both validators evaluate to valid.
Another example could be to validate multiples Credit Card numbers. In that case you have several regular expression patterns for each type of credit card. So the user can introduce a card number and if the information match with at least one pattern then the information is considered as valid.
final form = FormGroup({
'cardNumber': FormControl<String>(
validators: [
Validators.composeOR([
Validators.pattern(americanExpressCardPattern),
Validators.pattern(masterCardPattern),
Validators.pattern(visaCardPattern),
])
],
),
});
copied to clipboard
Groups of Groups 😁 #
FormGroup is not restricted to contains only FormControl, it can nest others FormGroup so you can create more complex Forms.
Supose you have a Registration Wizzard with several screens. Each screen collect specific information and at the end you want to collect all that information as one piece of data:
final form = FormGroup({
'personal': FormGroup({
'name': FormControl<String>(validators: [Validators.required]),
'email': FormControl<String>(validators: [Validators.required]),
}),
'phone': FormGroup({
'phoneNumber': FormControl<String>(validators: [Validators.required]),
'countryIso': FormControl<String>(validators: [Validators.required]),
}),
'address': FormGroup({
'street': FormControl<String>(validators: [Validators.required]),
'city': FormControl<String>(validators: [Validators.required]),
'zip': FormControl<String>(validators: [Validators.required]),
}),
});
copied to clipboard

Note how we have set the data type to a FormControl, although this is not mandatory when
declaring a Form, we highly recommend this syntax as good practice or to use the FormBuilder
syntax.

Using FormBuilder (read FormBuilder section below):
final form = fb.group({
'personal': fb.group({
'name': ['', Validators.required],
'email': ['', Validators.required],
}),
'phone': fb.group({
'phoneNumber': ['', Validators.required],
'countryIso': ['', Validators.required],
}),
'address': fb.group({
'street': ['', Validators.required],
'city': ['', Validators.required],
'zip': ['', Validators.required],
}),
});
copied to clipboard
You can collect all data using FormGroup.value:
void _printFormData(FormGroup form) {
print(form.value);
}
copied to clipboard
The previous method outputs a Map as the following one:
{
"personal": {
"name": "...",
"email": "..."
},
"phone": {
"phoneNumber": "...",
"countryIso": "..."
},
"address": {
"street": "...",
"city": "...",
"zip": "..."
}
}
copied to clipboard
And of course you can access to a nested FormGroup as following:
FormGroup personalForm = form.control('personal');
copied to clipboard
A simple way to create a wizard is for example to wrap a PageView within a ReactiveForm and each Page inside the PageView can contains a ReactiveForm to collect specific data.
Dynamic forms with FormArray #
FormArray is an alternative to FormGroup for managing any number of unnamed controls. As with FormGroup instances, you can dynamically insert and remove controls from FormArray instances, and the form array instance value and validation status is calculated from its child controls.
You don't need to define a key for each control by name, so this is a great option if you don't know the number of child values in advance.
Let's see a simple example:
final form = FormGroup({
'emails': FormArray<String>([]), // an empty array of emails
});
copied to clipboard
We have defined just an empty array. Let's define another array with two controls:
final form = FormGroup({
'emails': FormArray<String>([
FormControl<String>(value: '[email protected]'),
FormControl<String>(value: '[email protected]'),
]),
});
copied to clipboard

Note that you don't have to specify the name of the controls inside of the array.

If we output the value of the previous form group we will get something like this:
print(form.value);
copied to clipboard
{
"emails": ["[email protected]", "[email protected]"]
}
copied to clipboard
Let's dynamically add another control:
final array = form.control('emails') as FormArray<String>;

// adding another email
array.add(
FormControl<String>(value: '[email protected]'),
);

print(form.value);
copied to clipboard
{
"emails": ["[email protected]", "[email protected]", "[email protected]"]
}
copied to clipboard
Another way of add controls is to assign values directly to the array:
// Given: an empty array of strings
final array = FormArray<String>([]);

// When: set value to array
array.value = ["[email protected]", "[email protected]", "[email protected]"];

// Then: the array is no longer empty
expect(array.controls.length, 3);

// And: array has a control for each inserted value
expect(array.controls('0').value, "[email protected]");
expect(array.controls('1').value, "[email protected]");
expect(array.controls('2').value, "[email protected]");
copied to clipboard

To get a control from the array you must pass the index position as a String. This is because FormGroup and FormArray inherited from the same parent class and FormControl gets the controls by name (String).

A more advanced example:
// an array of contacts
final contacts = ['[email protected]', '[email protected]', '[email protected]'];

// a form with a list of selected emails
final form = FormGroup({
'selectedEmails': FormArray<bool>([], // an empty array of controls
validators: [emptyAddressee], // validates that at least one email is selected
),
});

// get the array of controls
final formArray = form.control('selectedEmails') as FormArray<bool>;

// populates the array of controls.
// for each contact add a boolean form control to the array.
formArray.addAll(
contacts.map((email) => FormControl<bool>(value: true)).toList(),
);
copied to clipboard
// validates that at least one email is selected
Map<String, dynamic> emptyAddressee(AbstractControl control) {
final emails = (control as FormArray<bool>).value;
return emails.any((isSelected) => isSelected)
? null
: {'emptyAddressee': true};
}
copied to clipboard
Arrays of Groups #
You can also create arrays of groups:
// an array of groups
final addressArray = FormArray([
FormGroup({
'city': FormControl<String>(value: 'Sofia'),
'zipCode': FormControl<int>(value: 1000),
}),
FormGroup({
'city': FormControl<String>(value: 'Havana'),
'zipCode': FormControl<int>(value: 10400),
}),
]);
copied to clipboard
Another example using FormBuilder:
// an array of groups using FormBuilder
final addressArray = fb.array([
fb.group({'city': 'Sofia', 'zipCode': 1000}),
fb.group({'city': 'Havana', 'zipCode': 10400}),
]);
copied to clipboard
or just:
// an array of groups using a very simple syntax
final addressArray = fb.array([
{'city': 'Sofia', 'zipCode': 1000},
{'city': 'Havana', 'zipCode': 10400},
]);
copied to clipboard
You can iterate over groups as follow:
final cities = addressArray.controls
.map((control) => control as FormGroup)
.map((form) => form.control('city').value)
.toList();
copied to clipboard

A common mistake is to declare an array of groups as FormArray<FormGroup>.
An array of FormGroup must be declared as FormArray() or as FormArray<Map<String, dynamic>>().

FormBuilder #
The FormBuilder provides syntactic sugar that shortens creating instances of a FormGroup, FormArray and FormControl. It reduces the amount of boilerplate needed to build complex forms.
Groups #
// creates a group
final form = fb.group({
'name': 'John Doe',
'email': ['', Validators.required, Validators.email],
'password': Validators.required,
});
copied to clipboard
The previous code is equivalent to the following one:
final form = FormGroup({
'name': FormControl<String>(value: 'John Doe'),
'email': FormControl<String>(value: '', validators: [Validators.required, Validators.email]),
'password': FormControl<String>(validators: [Validators.required]),
});
copied to clipboard
Arrays #
// creates an array
final aliases = fb.array(['john', 'little john']);
copied to clipboard
Control #
// creates a control of type String with a required validator
final control = fb.control<String>('', [Validators.required]);
copied to clipboard
Control state #
// create a group
final group = fb.group(
// creates a control with default value and disabled state
'name': fb.state(value: 'john', disabled: true),
);
copied to clipboard
Nested Controls #
To retrieves nested controls you can specify the name of the control as a dot-delimited string that define the path to the control:
final form = FormGroup({
'address': FormGroup({
'city': FormControl<String>(value: 'Sofia'),
'zipCode': FormControl<int>(value: 1000),
}),
});

// get nested control value
final city = form.control('address.city');

print(city.value); // outputs: Sofia
copied to clipboard
Reactive Form Widgets #
So far we have only defined our model-driven form, but how do we bind the form definition with our Flutter widgets? Reactive Forms Widgets is the answer ;)
Let's see an example:
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'name',
),
ReactiveTextField(
formControlName: 'email',
),
ReactiveTextField(
formControlName: 'password',
obscureText: true,
),
],
),
);
}
copied to clipboard

The example above ignores the emailConfirmation and passwordConfirmation fields previously seen for simplicity.

How to customize error messages? #
Validation messages can be defined at two different levels:

Reactive Widget level.
Global/Application level.

1. Reactive Widget level. #
Each reactive widget like ReactiveTextField, ReactiveDropdownField, and all others have the
property validationMessages as an argument of their constructors. In order to define custom
validation messages at widget level, just provide the property validationMessages with the
corresponding text values for each error as shown below:
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'name',
validationMessages: {
'required': (error) => 'The name must not be empty'
},
),
ReactiveTextField(
formControlName: 'email',
validationMessages: {
'required': (error) => 'The email must not be empty',
'email': (error) => 'The email value must be a valid email'
},
),
ReactiveTextField(
formControlName: 'password',
obscureText: true,
validationMessages: {
'required': (error) => 'The password must not be empty',
'minLength': (error) => 'The password must have at least 8 characters'
},
),
],
),
);
}
copied to clipboard

Reactive Forms have an utility class called ValidationMessage that brings access to
common validation messages: required, email, pattern and so on. So instead of write 'required' you
could use ValidationMessage.required as the key of validation messages:
return ReactiveTextField(
formControlName: 'email',
validationMessages: {
ValidationMessage.required: (error) => 'The email must not be empty',
ValidationMessage.email: (error) => 'The email value must be a valid email',
},
),
copied to clipboard
nice isn't it? ;)

2. Global/Application level. #
You can also define custom validation messages at a higher level, for example, at the application
level. When a reactive widget looks for an error message text, it first looks at widget level
definition, if it doesn't find any config at widget level then it looks at the global config
definition.
The global definition of validation messages allows you to define error messages in a centralized
way and relieves you to define validation messages on each reactive widget of your application.
In order to define these configs at a higher level use the widget ReactiveFormConfig and
define the validationMessages.
Here is an example of the global definition for custom validation messages:
Validation messages with error arguments: #
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return ReactiveFormConfig(
validationMessages: {
ValidationMessage.required: (error) => 'Field must not be empty',
ValidationMessage.email: (error) => 'Must enter a valid email',
},
child: MaterialApp(
home: Scaffold(
body: const Center(
child: Text('Hello Flutter Reactive Forms!'),
),
),
),
);
}
}
copied to clipboard
Parameterized validation messages #
You can enrich the validation messages using parameters of the error instance. In the next example
we are giving a more complete validation error to the user:
final form = FormGroup({
'password': FormControl<String>(
validators: [Validators.minLength(8)],
),
});
copied to clipboard
ReactiveTextField(
formControlName: 'password',
validationMessage: {
ValidationMessages.minLength: (error) =>
'The password must be at least ${(error as Map)['requiredLength']} characters long'
},
)
copied to clipboard
This will show the message: The password must be at least 8 characters long
When does Validation Messages begin to show up? #
Touching a control #
Even when the FormControl is invalid, validation messages will begin to show up when the FormControl is touched. That means when the user taps on the ReactiveTextField widget and then remove focus or completes the text edition.
You can initialize a FormControl as touched to force the validation messages to show up at the very first time the widget builds.
final form = FormGroup({
'name': FormControl<String>(
value: 'John Doe',
validators: [Validators.required],
touched: true,
),
});
copied to clipboard
When you set a value to a FormControl from code and want to show up validations messages
you must call FormControl.markAsTouched() method:
set name(String newName) {
final formControl = this.form.control('name');
formControl.value = newName;
formControl.markAsTouched();// if newName is invalid then validation messages will show up in UI
}
copied to clipboard

To mark all children controls of a FormGroup and FormArray you must call markAllAsTouched().
final form = FormGroup({
'name': FormControl<String>(
value: 'John Doe',
validators: [Validators.required],
touched: true,
),
});

// marks all children as touched
form.markAllAsTouched();
copied to clipboard

Overriding Reactive Widgets show errors behavior #
The second way to customize when to show error messages is to override the method showErrors in reactive widgets.
Let's suppose you want to show validation messages not only when it is invalid and touched (default behavior), but also when it's dirty:
ReactiveTextField(
formControlName: 'email',
// override default behavior and show errors when: INVALID, TOUCHED and DIRTY
showErrors: (control) => control.invalid && control.touched && control.dirty,
),
copied to clipboard

A control becomes dirty when its value change through the UI.
The method setErrors of the controls can optionally mark it as dirty too.

Enable/Disable Submit button #
For a better User Experience some times we want to enable/disable the Submit button based on the validity of the Form. Getting this behavior, even in such a great framework as Flutter, some times can be hard and can lead to have individual implementations for each Form of the same application plus boilerplate code.
We will show you two different approaches to accomplish this very easily:

Separating Submit Button in a different Widget.
Using ReactiveFormConsumer widget.

Separating Submit Button in a different Widget: #
Let's add a submit button to our Form:
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'email',
),
ReactiveTextField(
formControlName: 'password',
obscureText: true,
),
MySubmitButton(),
],
),
);
}
copied to clipboard

The above is a simple sign-in form with email, password, and a submit button.

Now let's see the implementation of the MySubmitButton widget:
class MySubmitButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final form = ReactiveForm.of(context);
return RaisedButton(
child: Text('Submit'),
onPressed: form.valid ? _onPressed : null,
);
}

void _onPressed() {
print('Hello Reactive Forms!!!');
}
}
copied to clipboard

Notice the use of ReactiveForm.of(context) to get access to the nearest FormGroup up the widget's tree.

In the previous example we have separated the implementation of the submit button in a different widget. The reasons behind this is that we want to re-build the submit button each time the validity of the FormGroup changes. We don't want to rebuild the entire Form, but just the button.
How is that possible? Well, the answer is in the expression:
final form = ReactiveForm.of(context);
copied to clipboard
The expression above have two important responsibilities:

Obtains the nearest FormGroup up the widget's tree.
Registers the current context with the changes in the FormGroup so that if the validity of the FormGroup changes then the current context is rebuilt.

Using ReactiveFormConsumer widget: #
ReactiveFormConsumer widget is a wrapped around the ReactiveForm.of(context) expression so that we can reimplement the previous example as follows:
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'email',
),
ReactiveTextField(
formControlName: 'password',
obscureText: true,
),
ReactiveFormConsumer(
builder: (context, form, child) {
return RaisedButton(
child: Text('Submit'),
onPressed: form.valid ? _onSubmit : null,
);
},
),
],
),
);
}

void _onSubmit() {
print('Hello Reactive Forms!!!');
}
copied to clipboard

It is entirely up to you to decide which of the above two approaches to use, but note that to access the FormGroup via ReactiveForm.of(context) the consumer widget must always be down in the tree of the ReactiveForm widget.

Focus/UnFocus a FormControl #
There are some cases where we want to add or remove focus on a UI TextField without the interaction of the user. For that particular cases you can use FormControl.focus() or FormControl.unfocus() methods.
final form = fb.group({'name': 'John Doe'});

FormControl control = form.control('name');

control.focus(); // UI text field get focus and the device keyboard pop up

control.unfocus(); // UI text field lose focus
copied to clipboard
You can also set focus directly from the Form like:
final form = fb.group({'name': ''});

form.focus('name'); // UI text field get focus and the device keyboard pop up
copied to clipboard
final form = fb.group({
'person': fb.group({
'name': '',
}),
});

// set focus to a nested control
form.focus('person.name');
copied to clipboard
Focus flow between Text Fields #
Another example is when you have a form with several text fields and each time the user completes edition in one field you want to request next focus field using the keyboard actions:
final form = fb.group({
'name': ['', Validators.required],
'email': ['', Validators.required, Validators.email],
'password': ['', Validators.required],
});
copied to clipboard
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'name',
textInputAction: TextInputAction.next,
onSubmitted: () => this.form.focus('email'),
),
ReactiveTextField(
formControlName: 'email',
textInputAction: TextInputAction.next,
onSubmitted: () => this.form.focus('password'),
),
ReactiveTextField(
formControlName: 'password',
obscureText: true,
),
],
),
);
}
copied to clipboard

When you remove focus of a control, the control is marked as touched, that means that the validation error messages will show up in UI. To prevent validation messages to show up you can optionally set argument touched to false.
// remove the focus to the control and marks it as untouched.
this.form.unfocus(touched: false);
copied to clipboard

How Enable/Disable a widget #
To disabled a widget like ReactiveTextField all you need to do is to mark the control as disabled:
final form = FormGroup({
'name': FormControl<String>(),
});

FormControl control = form.control('name');

// the control is disabled and also the widget in UI is disabled.
control.markAsDisabled();
copied to clipboard

When a control is disabled it is exempt from validation checks and excluded from the aggregate
value of any parent. Its status is DISABLED.
To retrieves all values of a FormGroup or FormArray regardless of disabled status in children use
FormControl.rawValue or FormArray.rawValue respectively.

How does ReactiveTextField differs from native TextFormField or TextField? #
ReactiveTextField has more in common with TextFormField that with TextField. As we all know TextFormField is a wrapper around the TextField widget that brings some extra capabilities such as Form validations with properties like autovalidate and validator. In the same way ReactiveTextField is a wrapper around TextField that handle the features of validations in a own different way.
ReactiveTextField has all the properties that you can find in a common TextField, it can be customizable as much as you want just as a simple TextField or a TextFormField. In fact must of the code was taken from the original TextFormField and ported to have a reactive behavior that binds itself to a FormControl in a two-way binding.
Below is an example of how to create some ReactiveTextField with some common properties:
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'name',
decoration: InputDecoration(
labelText: 'Name',
),
textCapitalization: TextCapitalization.words,
textAlign: TextAlign.center,
style: TextStyle(backgroundColor: Colors.white),
),
ReactiveTextField(
formControlName: 'phoneNumber',
decoration: InputDecoration(
labelText: 'Phone number',
),
keyboardType: TextInputType.number,
),
ReactiveTextField(
formControlName: 'password',
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
),
),
],
),
);
}
copied to clipboard

Because of the two-binding capability of the ReactiveTextField with a FormControl
the widget don't include properties as controller, validator, autovalidate, onSaved,
the FormControl is responsible for handling validation as well as changes
notifications.
It does include some events like onChanged, onTab, onEditingComplete,
and onSubmitted.

Supported Reactive Form Field Widgets #

ReactiveTextField
ReactiveDropdownField
ReactiveSwitch
ReactiveCheckbox
ReactiveRadio
ReactiveSlider
ReactiveCheckboxListTile
ReactiveSwitchListTile
ReactiveRadioListTile

Bonus Field Widgets #

ReactiveDatePicker
ReactiveTimePicker

Other Reactive Forms Widgets #

ReactiveForm
ReactiveFormConsumer
ReactiveFormBuilder
ReactiveFormArray
ReactiveValueListenableBuilder
ReactiveStatusListenableBuilder

Advanced Reactive Field Widgets #
We are trying to keep reactive_forms from bloating with third party dependencies this is why there is
a separate library reactive_forms_widgets which is under construction yet that provides
a variety of more advanced field widgets. To know more about how to install it please visit the library repo and read the documentation about the widgets it contains.

ReactiveAdvancedSwitch - wrapper around flutter_advanced_switch
ReactiveDateRangePicker - wrapper around showDateRangePicker
ReactiveDateTimePicker - wrapper around showDatePicker and showTimePicker
ReactiveDropdownSearch - wrapper around dropdown_search
ReactiveFilePicker - wrapper around file_picker
ReactiveImagePicker - wrapper around image_picker
ReactiveMultiImagePicker - wrapper around multi_image_picker
ReactiveSegmentedControl - wrapper around CupertinoSegmentedControl
ReactiveSignature - wrapper around signature
ReactiveTouchSpin - wrapper around flutter_touch_spin
ReactiveRangeSlider - wrapper around RangeSlider
ReactiveSleekCircularSlider - wrapper around sleek_circular_slider
ReactiveCupertinoTextField - wrapper around CupertinoTextField
ReactiveRatingBar - wrapper around flutter_rating_bar
ReactiveMacosUi - wrapper around macos_ui
ReactivePinPut - wrapper around pinput
ReactiveCupertinoSwitch - wrapper around CupertinoSwitch
ReactivePinCodeTextField - wrapper around pin_code_fields
ReactiveSlidingSegmentedControl - wrapper around CupertinoSlidingSegmentedControl
ReactiveCupertinoSlider - wrapper around CupertinoSlider
ReactiveColorPicker - wrapper around flutter_colorpicker
ReactiveMonthPickerDialog - wrapper around month_picker_dialog
ReactiveRawAutocomplete - wrapper around RawAutocomplete
ReactiveFlutterTypeahead - wrapper around flutter_typeahead
ReactivePinInputTextField - wrapper around pin_input_text_field
ReactiveDirectSelect - wrapper around direct_select
ReactiveMarkdownEditableTextInput - wrapper around markdown_editable_textinput
ReactiveCodeTextField - wrapper around code_text_field
ReactivePhoneFormField - wrapper around phone_form_field
ReactiveExtendedTextField - wrapper around extended_text_field
ReactiveCupertinoSlidingSegmentedControl - wrapper around CupertinoSlidingSegmentedControl

ReactiveTextField #
We have explain the common usage of a ReactiveTextField along this documentation.
ReactiveDropdownField #
ReactiveDropdownField as all the other reactive field widgets is almost the same as its native version DropdownButtonFormField but adding two-binding capabilities. The code is ported from the original native implementation. It have all the capability of styles and themes of the native version.
final form = FormGroup({
'payment': FormControl<int>(validators: [Validators.required]),
});

@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveDropdownField<int>(
formControlName: 'payment',
hint: Text('Select payment...'),
items: [
DropdownMenuItem(
value: 0,
child: Text('Free'),
),
DropdownMenuItem(
value: 1,
child: Text('Visa'),
),
DropdownMenuItem(
value: 2,
child: Text('Mastercard'),
),
DropdownMenuItem(
value: 3,
child: Text('PayPal'),
),
],
),
],
),
);
}
copied to clipboard

As you can see from the above example the usage of ReactiveDropdownField is almost the same as the usage of a common DropdownButtonFormField, except for the additional formControlName and validationMessages properties.

ReactiveValueListenableBuilder to listen when value changes in a FormControl #
If you want to rebuild a widget each time a FormControl value changes you could use the ReactiveValueListenableBuilder widget.
In the following example we are listening for changes in lightIntensity. We change that value with a ReactiveSlider and show all the time the value in a Text widget:
final form = FormGroup({
'lightIntensity': FormControl<double>(value: 50.0),
});

@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveValueListenableBuilder<double>(
formControlName: 'lightIntensity',
builder: (context, value, child) {
return Text('lights at ${value?.toStringAsFixed(2)}%');
},
),
ReactiveSlider(
formControlName: 'lightIntensity',
max: 100.0,
),
],
)
);
}
copied to clipboard
ReactiveForm vs ReactiveFormBuilder which one? #
Both widgets are responsible for exposing the FormGroup to descendants widgets in the tree. Let see an example:
// using ReactiveForm
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: ReactiveTextField(
formControlName: 'email',
),
);
}
copied to clipboard
// using ReactiveFormBuilder
@override
Widget build(BuildContext context) {
return ReactiveFormBuilder(
form: () => this.form,
builder: (context, form, child) {
return ReactiveTextField(
formControlName: 'email',
);
},
);
}
copied to clipboard
The main differences are that ReactiveForm is a StatelessWidget so it doesn't save the instance of the FormGroup. You must declare the instance of the FormGroup in a StatefulWidget or resolve it from some Provider (state management library).
// Using ReactiveForm in a StatelessWidget and resolve the FormGroup from a provider
class SignInForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<SignInViewModel>(context, listen: false);

return ReactiveForm(
formGroup: viewModel.form,
child: ReactiveTextField(
formControlName: 'email',
),
);
}
}
copied to clipboard
// Using ReactiveForm in a StatefulWidget and declaring FormGroup in the state.
class SignInForm extends StatefulWidget {
@override
_SignInFormState createState() => _SignInFormState();
}

class _SignInFormState extends State<SignInForm> {
final form = fb.group({
'email': Validators.email,
});

@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: this.form,
child: ReactiveTextField(
formControlName: 'email',
),
);
}
}
copied to clipboard

If you declare a FormGroup in a StatelessWidget the group will be destroyed a created each time the instance of the StatelessWidget is destroyed and created, so you must preserve the FormGroup in a state or in a Bloc/Provider/etc.

By the other hand ReactiveFormBuilder is implemented as a StatefulWidget so it holds the created FormGroup in its state. That way is safe to declares the FormGroup in a StatelessWidget or get it from a Bloc/Provider/etc.
class SignInForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReactiveFormBuilder(
form: () => fb.group({'email': Validators.email}),
builder: (context, form, child) {
return ReactiveTextField(
formControlName: 'email',
);
},
);
}
}
copied to clipboard
You should use ReactiveForm if:

The form is complex enough.
You need to listen for changes in some child control to execute some business logic.
You are using some State Management library like Provider or Bloc.
Using a StatefulWidget to declare a very simple form is something that really doesn't bother you.

You should use ReactiveFormBuilder if:

The form is quite simple enough and doesn't need a separate Provider/Bloc state.
You don't want to use a StatefulWidget to declare the FormGroup.

But the final decision is really up to you, you can use any of them in any situations ;)
Widget testing #
note: mark your fields with Key's for easy access via widget tester
example component #
class LoginForm extends StatefulWidget {
const LoginForm({Key? key}) : super(key: key);

@override
LoginFormState createState() => LoginFormState();
}

class LoginFormState extends State<LoginForm> {
final form = FormGroup({
'email': FormControl<String>(validators: [Validators.required, Validators.email]),
'password': FormControl<String>(validators: [Validators.required]),
});

@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: form,
child: Column(
children: <Widget>[
ReactiveTextField(
key: const Key('email'),
formControlName: 'email',
),
ReactiveTextField(
key: const Key('password'),
formControlName: 'password',
obscureText: true,
),
ElevatedButton(
key: const Key('submit'),
onPressed: () {},
child: const Text('Submit'),
),
],
),
);
}
}
copied to clipboard
example test #

void main() {
testWidgets('LoginForm should pass with correct values', (tester) async {
// Build the widget.
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: LoginForm()),
));

await tester.enterText(find.byKey(const Key('email')), '[email protected]');
await tester.enterText(find.byKey(const Key('password')), 'password');

await tester.tap(find.byKey(const Key('submit')));

await tester.pump();

// Expect to find the item on screen if needed
expect(find.text('[email protected]'), findsOneWidget);

// Get form state
final LoginFormState loginFormState = tester.state(find.byType(LoginForm));

// Check form state
expect(loginFormState.form.valid, true);
});
}

copied to clipboard
Reactive Forms + Provider plugin 💪 #
Although Reactive Forms can be used with any state management library or even without any one at all, Reactive Forms gets its maximum potential when is used in combination with a state management library like the Provider plugin.
This way you can separate UI logic from business logic and you can define the FormGroup inside a business logic class and then exposes that class to widgets with mechanism like the one Provider plugin brings.
Reactive Forms + code generation 🤖 #
ReactiveFormsGenerator is the code generator for reactive_forms which will save you tons of time and make your forms type safe.
There is no reason write code manually! Let the code generation work for you.
How create a custom Reactive Widget? #
Reactive Forms is not limited just to common widgets in Forms like text, dropdowns, sliders switch fields and etc, you can easily create custom widgets that two-way binds to FormControls and create your own set of Reactive Widgets ;)
In our Wiki you can find a tutorial of how to create your custom Reactive Widget.
You can also check Star Rating with Flutter Reactive Forms post as another example of a custom reactive widget.
What is not Reactive Forms #


Reactive Forms is not a fancy widgets package. It is not a library that brings some new Widgets with new shapes, colors or animations. It lets you to decide the shapes, colors, and animations you want for your widgets, but frees you from the responsibility of gathering and validating the data. And keeps the data in sync between your model and your widgets.


Reactive Forms does not pretend to replace the native widgets that you commonly use in your Flutter projects like TextFormField, DropdownButtonFormField or CheckboxListTile. Instead of that it brings new two-way binding capabilities and much more features to those same widgets.


What is Reactive Forms #

Reactive Forms provides a model-driven approach to handling form inputs whose values change over time. It's heavily inspired in Angular Reactive Form.
It lets you focus on business logic and save you time from collect, validate and mantain synchronization between your models and widgets.
Remove boilerplate code and brings you the posibility to write clean code defining a separation between model and UI with minimal efforts.
And it integrates perfectly well with common state management libraries like Provider, Bloc and many others good libraries the community has created.

Migrate versions #
Visit Migration Guide to see
more details about different version breaking changes.

License:

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.