reactive_forms

v18.2.2

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

Архив пакета: https://pubdev.letsnova.ru/api/archives/reactive_forms/18.2.2.tar.gz

Установкаdart pub add reactive_forms

README

Reactive Forms

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

Pub Version GitHub GitHub top language flutter tests Codacy Badge codecov

Table of Contents

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.7.0
  • Flutter: ">=3.29.0"

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

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

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

Reactive Forms v8.x includes the intl package. If a version conflict is present, 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 can install this package by adding reactive_forms to the dependencies list of your pubspec.yaml file as follows:

dependencies:
                  flutter:
                    sdk: flutter
                
                  reactive_forms: ^18.2.2
                

Then, run the command flutter packages get in the console.

Creating a Form

A form is composed of multiple fields or controls.

To declare a form with the fields name and email, it's as simple as:

final form = FormGroup({
                  'name': FormControl<String>(value: 'John Doe'),
                  'email': FormControl<String>(),
                });
                

Default Values

Notice in the example above that for the name field, we have set a default value. For the email field, 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: 'johndoe@email.com'),
                });
                

You can get the value of a single FormControl as simply as:

String get name => this.form.control('name').value;
                

You can also get the complete Form data as follows:

print(form.value);
                

The previous code prints the following output:

{
                  "name": "John Doe",
                  "email": "johndoe@email.com"
                }
                

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

To set values to controls, you can use two approaches:

// Set the value directly to the control
                this.form.control('name').value = 'John';
                
                // Set values to controls by setting the value to the form
                this.form.value = {
                  'name': 'John',
                  'email': 'john@email.com',
                };
                

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,
                  ]),
                });
                

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

There are common predefined validators, but you can also implement custom validators.

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
  • Validators.oneOf

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. To implement a custom validator, you can follow two different approaches:

  1. Extend the Validator class and override the validate method.
  2. Implement a custom validator function/method and use it with the Validators.delegate(...) validator.

Let's implement a custom validator that validates that a control's value must be true:

Inheriting from the Validator class:

Let's create a class that extends 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};
                  }
                }
                

The validate 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, it returns a Map with the error key and custom information. In the previous example, we defined requiredTrue as the error key and true as the custom information.

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
                    ],
                  ),
                });
                

Using the Validators.delegate() validator:

Sometimes, it's more convenient to implement a custom validator in a separate method/function than in a new class. In that case, it's 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
                    ],
                  ),
                });
                
/// Custom function that validates that the 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};
                }
                

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 has always been 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);
                

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

If we print the value of FormControl.errors:

print(cardNumber.errors);
                

We will get a Map like this:

{
                  "pattern": {
                    "requiredPattern": "^3[47][0-9]{13}$",
                    "actualValue": 395465465421
                  }
                }
                

FormGroup Validators

There are special validators that can be attached to a 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 the validation of one field depends on the value of another. For example, a sign-up form with email and emailConfirmation or password and passwordConfirmation.

For those cases, we can 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')
                ]);
                

Notice the use of Validators.minLength(8)

In the previous code, we added two more fields to the form: password and passwordConfirmation. Both fields are required, and the password must be at least 8 characters long.

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 is as follows:

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;
                  }
                }
                

Fortunately, you don't have to implement a custom must match validator because we have already included it in the reactive_forms package, so you should reuse it. The previous form definition becomes:

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'),
                ]);
                

Asynchronous Validators :sunglasses:

Sometimes, you want to perform a validation against a remote server. These 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 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 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 louder than words, so let's see an example.

Let's implement the previously mentioned example: the user is typing an 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 :)
                    ],
                  ),
                });
                

We have declared a simple Form with an email field that is required and must have a valid email value. We have also included 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 by 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 (e.g., a server request).
                  Future<bool> _getIsUniqueEmail(String email) {
                    // Simple array that simulates emails stored in the server DB.
                    final storedEmails = ['johndoe@email.com', 'john@email.com'];
                
                    return Future.delayed(
                      const Duration(seconds: 5),
                      () => !storedEmails.contains(email),
                    );
                  }
                }
                

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 provide feedback to the user, we strongly advise you to visit our Wiki. We have not included the full example in this README.md file to simplify things here and not to anticipate things that we will see later in this doc.

The validator Validators.delegateAsync() is another way to implement a 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 optional argument in the FormControl constructor.

final control = FormControl<String>(
                  asyncValidators: [UniqueEmailAsyncValidator()],
                  asyncValidatorsDebounceTime: 1000, // Sets a 1-second debounce time.
                );
                

Custom Debounce Time in Async Validators

You can also specify a custom debounce time for a single async validator. This is useful when you have multiple async validators with different debounce time requirements.

final control = FormControl<String>(
                  asyncValidators: [
                    Validators.debounced(
                      Validators.delegateAsync((control) async {
                        // Your validation logic here
                        return null;
                      }),
                      500, // Debounce time in milliseconds
                    ),
                  ],
                );
                

Custom Debounce Time in delegateAsync Validator

The Validators.delegateAsync() function now accepts an optional debounceTime parameter, defaulting to 0. This allows for immediate execution or custom debouncing for asynchronous validation.

final form = fb.group({
                  'userName': FormControl<String>(
                    asyncValidators: [
                      Validators.delegateAsync((control) async {
                        // Simulate a call to a backend service
                        await Future<void>.delayed(Duration(seconds: 1));
                        if (control.value == 'existingUser') {
                          return {'unique': true};
                        }
                        return null;
                      }, debounceTime: 300),
                    ],
                  ),
                });
                

You can also use it without a debounce time:

final form = fb.group({
                  'userName': FormControl<String>(
                    asyncValidators: [
                      Validators.delegateAsync((control) async {
                        // Simulate a call to a backend service
                        await Future<void>.delayed(Duration(seconds: 1));
                        if (control.value == 'existingUser') {
                          return {'unique': true};
                        }
                        return null;
                      }), // No debounce time
                    ],
                  ),
                });
                

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 the 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),
                      ])
                    ],
                  ),
                });
                

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

With Validators.composeOR, we are telling the FormControl that if at least one validator evaluates as VALID, then the control is VALID. It's not necessary for both validators to evaluate to valid.

Another example could be to validate multiple 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 matches at least one pattern, then the information is considered valid.

final form = FormGroup({
                  'cardNumber': FormControl<String>(
                    validators: [
                      Validators.composeOR([
                        Validators.pattern(americanExpressCardPattern),
                        Validators.pattern(masterCardPattern),
                        Validators.pattern(visaCardPattern),
                      ])
                    ],
                  ),
                });
                

One Of Validator

The oneOf validator is used to validate that the control's value is one of the values in the provided collection. For String values, the comparison can be made case-sensitive or insensitive.

final form = FormGroup({
                  'fruit': FormControl<String>(
                    validators: [
                      Validators.oneOf(['apple', 'banana', 'orange']),
                    ],
                  ),
                });
                

Groups of Groups :grin:

FormGroup is not restricted to containing only FormControl; it can nest other FormGroups, so you can create more complex Forms.

Suppose you have a Registration Wizard with several screens. Each screen collects 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]),
                  }),
                });
                

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 a good practice or to use the FormBuilder syntax.

Using FormBuilder (read the 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],
                  }),
                });
                

You can collect all data using FormGroup.value:

void _printFormData(FormGroup form) {
                  print(form.value);
                }
                

The previous method outputs a Map like the following one:

{
                  "personal": {
                    "name": "...",
                    "email": "..."
                  },
                  "phone": {
                    "phoneNumber": "...",
                    "countryIso": "..."
                  },
                  "address": {
                    "street": "...",
                    "city": "...",
                    "zip": "..."
                  }
                }
                

And of course, you can access a nested FormGroup as follows:

FormGroup personalForm = form.control('personal');
                

A simple way to create a wizard is, for example, to wrap a PageView within a ReactiveForm, and each Page inside the PageView can contain 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 are 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
                });
                

We have defined just an empty array. Let's define another array with two controls:

final form = FormGroup({
                  'emails': FormArray<String>([
                    FormControl<String>(value: 'john@email.com'),
                    FormControl<String>(value: 'susan@email.com'),
                  ]),
                });
                

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

If we output the value of the previous form group, we will get something like this:

print(form.value);
                
{
                  "emails": ["john@email.com", "susan@email.com"]
                }
                

Let's dynamically add another control:

final array = form.control('emails') as FormArray<String>;
                
                // Adding another email
                array.add(
                  FormControl<String>(value: 'caroline@email.com'),
                );
                
                print(form.value);
                
{
                  "emails": ["john@email.com", "susan@email.com", "caroline@email.com"]
                }
                

Another way to add controls is to assign values directly to the array:

// Given: an empty array of strings
                final array = FormArray<String>([]);
                
                // When: set value to the array
                array.value = ["john@email.com", "susan@email.com", "caroline@email.com"];
                
                // Then: the array is no longer empty
                expect(array.controls.length, 3);
                
                // And: the array has a control for each inserted value
                expect(array.controls('0').value, "john@email.com");
                expect(array.controls('1').value, "susan@email.com");
                expect(array.controls('2').value, "caroline@email.com");
                

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

A more advanced example:

// An array of contacts
                final contacts = ['john@email.com', 'susan@email.com', 'caroline@email.com'];
                
                // 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(),
                );
                
// 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};
                }
                

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),
                  }),
                ]);
                

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}),
                ]);
                

or just:

// An array of groups using a very simple syntax
                final addressArray = fb.array([
                  {'city': 'Sofia', 'zipCode': 1000},
                  {'city': 'Havana', 'zipCode': 10400},
                ]);
                

You can iterate over groups as follows:

final cities = addressArray.controls
                        .map((control) => control as FormGroup)
                        .map((form) => form.control('city').value)
                        .toList();
                

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

FormBuilder

The FormBuilder provides syntactic sugar that shortens the creation of 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,
                });
                

The previous code is equivalent to the following:

final form = FormGroup({
                  'name': FormControl<String>(value: 'John Doe'),
                  'email': FormControl<String>(value: '', validators: [Validators.required, Validators.email]),
                  'password': FormControl<String>(validators: [Validators.required]),
                });
                

Arrays

// Creates an array
                final aliases = fb.array(['john', 'little john']);
                

Control

// Creates a control of type String with a required validator
                final control = fb.control<String>('', [Validators.required]);
                

Control State

// Create a group
                final group = fb.group(
                  // Creates a control with a default value and disabled state
                  'name': fb.state(value: 'john', disabled: true),
                );
                

Nested Controls

To retrieve nested controls, you can specify the name of the control as a dot-delimited string that defines 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
                

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 Form Widgets are 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,
                        ),
                      ],
                    ),
                  );
                }
                

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:

  1. Reactive Widget level.
  2. Global/Application level.

1. Reactive Widget Level.

Each reactive widget, like ReactiveTextField, ReactiveDropdownField, and all others, has the validationMessages property as an argument of their constructors. To define custom validation messages at the widget level, just provide the validationMessages property 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'
                          },
                        ),
                      ],
                    ),
                  );
                }
                

Reactive Forms has a utility class called ValidationMessage that provides access to common validation messages: required, email, pattern, and so on. So instead of writing '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',
                   },
                );
                

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 the widget-level definition. If it doesn't find any config at the 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 from defining validation messages on each reactive widget of your application.

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!'),
                          ),
                        ),
                      ),
                    );
                  }
                }
                

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)],
                  ),
                });
                
ReactiveTextField(
                  formControlName: 'password',
                  validationMessage: {
                    ValidationMessages.minLength: (error) =>
                    'The password must be at least ${(error as Map)['requiredLength']} characters long'
                  },
                )
                

This will show the message: The password must be at least 8 characters long

When do Validation Messages Appear?

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 removes focus or completes the text editing.

You can initialize a FormControl as touched to force the validation messages to show up the very first time the widget builds.

final form = FormGroup({
                  'name': FormControl<String>(
                    value: 'John Doe',
                    validators: [Validators.required],
                    touched: true,
                  ),
                });
                

When you set a value to a FormControl from code and want to show validation messages, you must call the 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 the UI
                }
                

To mark all children controls of a FormGroup and FormArray as touched, 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();
                

Overriding Reactive Widgets Show Errors Behavior

The second way to customize when to show error messages is to override the showErrors method 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,
                ),
                

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

Enable/Disable Submit Button

For a better User Experience, sometimes we want to enable/disable the Submit button based on the validity of the Form. Achieving this behavior, even in a great framework like Flutter, can sometimes be hard and can lead to individual implementations for each Form of the same application, plus boilerplate code.

We will show you two different approaches to accomplish this very easily:

  1. Separating the Submit Button into a different Widget.
  2. Using the ReactiveFormConsumer widget.

Separating Submit Button in a Separate 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(),
                      ],
                    ),
                  );
                }
                

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!!!');
                  }
                }
                

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

In the previous example, we separated the implementation of the submit button into a different widget. The reason behind this is that we want to rebuild the submit button each time the validity of the FormGroup changes. We don't want to rebuild the entire Form, just the button.

How is that possible? Well, the answer is in the expression:

final form = ReactiveForm.of(context);
                

The expression above has 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, the current context is rebuilt.

Using the ReactiveFormConsumer widget:

The ReactiveFormConsumer widget is a wrapper 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!!!');
                }
                

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 user's interaction. For those particular cases, you can use the FormControl.focus() or FormControl.unfocus() methods.

final form = fb.group({'name': 'John Doe'});
                
                FormControl control = form.control('name');
                
                control.focus(); // UI text field gets focus, and the device keyboard pops up
                
                control.unfocus(); // UI text field loses focus
                

You can also set focus directly from the Form like this:

final form = fb.group({'name': ''});
                
                form.focus('name'); // UI text field gets focus, and the device keyboard pops up
                
final form = fb.group({
                  'person': fb.group({
                    'name': '',
                  }),
                });
                
                // Set focus to a nested control
                form.focus('person.name');
                

Focus Flow Between Text Fields

Another example is when you have a form with several text fields, and each time the user completes editing in one field, you want to request the next focus field using keyboard actions:

final form = fb.group({
                  'name': ['', Validators.required],
                  'email': ['', Validators.required, Validators.email],
                  'password': ['', Validators.required],
                });
                
@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,
                        ),
                      ],
                    ),
                  );
                }
                

When you remove focus from a control, the control is marked as touched. This means that the validation error messages will show up in the UI. To prevent validation messages from showing up, you can optionally set the touched argument to false.

// Remove the focus from the control and mark it as untouched.
                this.form.unfocus(touched: false);
                

How to Enable/Disable a Widget

To disable a widget like ReactiveTextField, all you need to do is mark the control as disabled:

final form = FormGroup({
                  'name': FormControl<String>(),
                });
                
                FormControl control = form.control('name');
                
                // The control is disabled, and the widget in the UI is also disabled.
                control.markAsDisabled();
                

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 retrieve all values of a FormGroup or FormArray regardless of the disabled status of children, use FormControl.rawValue or FormArray.rawValue, respectively.

How does ReactiveTextField differ from native TextFormField or TextField?

ReactiveTextField has more in common with TextFormField than 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 handles the features of validations in its own different way.

ReactiveTextField has all the properties that you can find in a common TextField. It can be customized as much as you want, just like a simple TextField or a TextFormField. In fact, most 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',
                          ),
                        ),
                      ],
                    ),
                  );
                }
                

Because of the two-way binding capability of the ReactiveTextField with a FormControl, the widget does not include properties like controller, validator, autovalidate, or onSaved. The FormControl is responsible for handling validation as well as change notifications.

It does include some events like onChanged, onTap, 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 still under construction, 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.

ReactiveTextField

We have explained the common usage of a ReactiveTextField throughout this documentation.

ReactiveDropdownField

ReactiveDropdownField, like all the other reactive field widgets, is almost the same as its native version, DropdownButtonFormField, but adds two-way binding capabilities. The code is ported from the original native implementation. It has 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'),
                            ),
                          ],
                        ),
                      ],
                    ),
                  );
                }
                

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 for Value Changes in a FormControl

If you want to rebuild a widget each time a FormControl value changes, you can use the ReactiveValueListenableBuilder widget.

In the following example, we are listening for changes in lightIntensity. We change that value with a ReactiveSlider and show the value in a Text widget all the time:

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,
                        ),
                      ],
                    )
                  );
                }
                

ReactiveForm vs. ReactiveFormBuilder: Which to Choose?

Both widgets are responsible for exposing the FormGroup to descendant widgets in the tree. Let's see an example:

// Using ReactiveForm
                @override
                Widget build(BuildContext context) {
                  return ReactiveForm(
                    formGroup: this.form,
                    child: ReactiveTextField(
                      formControlName: 'email',
                    ),
                  );
                }
                
// Using ReactiveFormBuilder
                @override
                Widget build(BuildContext context) {
                  return ReactiveFormBuilder(
                    form: () => this.form,
                    builder: (context, form, child) {
                      return ReactiveTextField(
                        formControlName: 'email',
                      );
                    },
                  );
                }
                

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 resolving 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',
                      ),
                    );
                  }
                }
                
// Using ReactiveForm in a StatefulWidget and declaring the 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',
                      ),
                    );
                  }
                }
                

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

On the other hand, ReactiveFormBuilder is implemented as a StatefulWidget, so it holds the created FormGroup in its state. That way, it's safe to declare 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',
                        );
                      },
                    );
                  }
                }
                

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 a 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 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 situation.

Widget Testing

Note: Mark your fields with Keys for easy access via the 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'),
                          ),
                        ],
                      ),
                    );
                  }
                }
                

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')), 'etc@test.qa');
                    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('etc@test.qa'), findsOneWidget);
                
                    // Get form state
                    final LoginFormState loginFormState = tester.state(find.byType(LoginForm));
                
                    // Check form state
                    expect(loginFormState.form.valid, true);
                  });
                }
                

Reactive Forms + Provider plugin :muscle:

Although Reactive Forms can be used with any state management library or even without one at all, Reactive Forms reaches its maximum potential when 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 expose that class to widgets with a mechanism like the one the Provider plugin provides.

Reactive Forms + Code Generation 🤖

ReactiveFormsGenerator is the code generator for reactive_forms that will save you tons of time and make your forms type-safe.

There is no reason to write code manually! Let the code generation work for you.

How to Create a Custom Reactive Widget?

Reactive Forms is not limited to just common widgets in Forms like text, dropdowns, sliders, switch fields, etc. You can easily create custom widgets that two-way bind to FormControls and create your own set of Reactive Widgets.

In our Wiki, you can find a tutorial on how to create your custom Reactive Widget.

You can also check the Star Rating with Flutter Reactive Forms post as another example of a custom reactive widget.

What Reactive Forms is Not

  • Reactive Forms is not a fancy widgets package. It is not a library that brings new Widgets with new shapes, colors, or animations. It lets you decide the shapes, colors, and animations you want for your widgets but frees you from the responsibility of gathering and validating the data. It also 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, it brings new two-way binding capabilities and many more features to those same widgets.

What Reactive Forms Is

  • Reactive Forms provides a model-driven approach to handling form inputs whose values change over time. It's heavily inspired by Angular Reactive Forms.
  • It lets you focus on business logic and saves you time from collecting, validating, and maintaining synchronization between your models and widgets.
  • It removes boilerplate code and gives you the possibility to write clean code by defining a separation between model and UI with minimal effort.
  • It integrates perfectly well with common state management libraries like Provider, Bloc, and many other good libraries the community has created.

Migration Versions

Visit the Migration Guide to see more details about different version breaking changes.

История изменений

18.2.2

Fixes

  • Correct hasErrors to check child controls.

Enhances

  • Improve errors getter performance.
  • Optimize list and map access.

18.2.1

Enhances

  • Added optional parameters to the clearValidators method to support automatic validation after clearing validators.

18.2.0

Features

  • Allowed overwriting default value on reset. Added an overwriteDefaultValue parameter to the FormControl.reset() method. When overwriteDefaultValue is true, the value passed to reset() becomes the new default value for the control.
  • Enhanced FormControl reset method. Introduced a nonNullable property to the FormControl constructor to control the behavior of the reset() method. When nonNullable is true (the default), calling reset() without a value will reset the control to its initial value. When nonNullable is false, calling reset() without a value will reset the control to null.
  • Exposed defaultValue of the FormControl.

18.1.2

Fixes

  • Fixed an issue where enabling or disabling a FormGroup and FormArray did not correctly propagate the enabled/disabled state to all of its child controls.

18.1.1

Fixes

  • Fix NumberValidator to correctly handle decimal numbers with trailing zeros.
  • Improve NumberValidator to correctly validate numbers with multiple decimal points, single dot, and dot at the end.

18.1.0

  • Add oneOf validator to the list of validators.
  • Add debounced async validator that allows to specify a custom debounce time for a single validator.
  • The Validators.delegateAsync() function now accepts an optional debounceTime parameter, defaulting to 0. This allows immediate execution or custom debouncing for asynchronous validation.

18.0.1

Features

  • Add allowNull optional parameter to the CompareValidator.
  • The FormControl.reset() method has been updated to align with the common expectation that resetting a control without specifying a new value should revert it to its initial state.
  • The reset method in AbstractControl (and by inheritance FormControl) has been updated to correctly handle the disabled state. If the disabled parameter is not provided to the reset method, the control's disabled status will now revert to the initial disabled value that was set when the control was constructed.

Fixes

  • Fixed broken reactivity with the PopScope.

18.0.0

  • Moved examples project out of the package's code, and uploaded to its own repo.

Breaking changes

  • Upgraded Flutter minimum version to 3.29.0.
  • Added missing fields to widgets:
    • ReactiveCheckbox: semanticLabel.
    • ReactiveCheckboxListTile: checkboxSemanticLabel.
    • ReactiveDatePicker: barrierColor, barrierDismissible, barrierLabel, switchToInputEntryModeIcon, switchToCalendarEntryModeIcon, onDatePickerModeChange.
    • ReactiveTimePicker: barrierColor, barrierDismissible, barrierLabel, orientation, switchToInputEntryModeIcon, switchToTimerEntryModeIcon.
    • ReactiveForm: onPopInvokedWithResult (used in PopScope), removed deprecated onPopInvoked.
    • ReactiveFormPopScope: onPopInvokedWithResult (used in PopScope), removed deprecated onPopInvoked.
    • ReactiveFormBuilder: onPopInvokedWithResult (used in PopScope), removed deprecated onPopInvoked.
    • ReactiveRadioListTile: radioScaleFactor.
    • ReactiveSlider: padding, allowedInteraction.
    • ReactiveSwitch: padding, trackOutlineWidth.
    • ReactiveSwitch.adaptive: thumbIcon, trackOutlineWidth, trackOutlineColor, padding, applyCupertinoTheme, onFocusChange.
    • ReactiveTextField: groupId, ignorePointers, cursorErrorColor, stylusHandwritingEnabled (removed deprecated scribbleEnabled), onTapAlwaysCalled, onTapUpOutside, statesController
  • Renamed ReactiveSwitchListTile.adaptative to ReactiveSwitchListTile.adaptive

Fixes

  • Added const keyword to DefaultPatternEvaluator and RegExpPatternEvaluator constructors.

17.0.1

  • Reduce intl dependency constraint to match Flutter 3.16.0.
  • NumberValidator allows null values.

17.0.0

Breaking changes

  • Removed deprecated onWillPop from ReactiveForm and ReactiveFormBuilder widgets. It was replaced with the PopScope widget.
  • Validators.number allows now to define negative numbers and decimal numbers with the addition of two optional arguments allowNegatives and allowedDecimals.

Features

  • Add canPop and onPopInvoked to ReactiveForm and ReactiveFormBuilder widgets.

16.1.1

Fixes

  • Add default Context Menu to ReactiveTextField.

16.1.0

Features

  • Add markAsPending() method to AbstractControl to allow set the status to PENDING by demand.

16.0.4

Fixes

  • Add missing properties to ReactiveSwitchListTile.adaptative() widget.
  • Add showError() to ReactiveCheckbox and ReactiveCheckboxListTile widgets. This does not display any error messages but it is now compatible with the Flutter builtin behavior of Checkboxes when Material 3 is enabled (ThemeData(useMaterial3: true)) in the active App Theme.

Enhances

  • Update Readme.md file with testing examples in the section ReactiveForm vs ReactiveFormBuilder which one?

16.0.3

Fixes

  • Fix an issue with FormGroup and FormArray were recursive Raw Value was not working properly.

16.0.2

Fixes

  • Fix an issue with FormBuilder when trying to build a control with a nullable '?' data type.
  • Fix an issue with FormGroup that was not triggering the event collectionChanges when a control is removed.
  • Fix an issue with FormGroup and FormArray when trying to find a control with a nullable '?' data type.

16.0.1

Fixes

  • Update the intl dependency version, because in flutter 3.10, flutter_localizations depends on intl 0.18.0.

16.0.0

Breaking Changes

  • Flutter >= 3.10 required for this version.

Fixes

  • Updated some documentation.
  • Expose validator classes to allow direct instantiation.

15.0.0

Breaking Changes

  • All validators have been changed to classes with const constructors.
  • The Asynchronous Validator is now a class from where any custom async validator can inherit.

Features

  • A new validator Validators.delegate(...) has been introduced to be used with a custom validation function.
  • A new validator Validators.delegateAsync(...) has been introduced to be used with a custom async validation functions.

14.3.0

  • Fix the inkwell ripple effect in the ReactiveDropdownField.
  • Add some other minor fixes.

14.2.0

  • Update intl to latest version 0.18.0.

14.1.0

Enhances

  • Create new widget ReactiveFocusableFormField as a parent widget for all other widgets that requires to do focus management.

14.0.0

Breaking Changes

  • The definition of validation messages is now more consistent.
  • Methods like onTab in ReactiveTextField and onChanged in ReactiveDropdownField now provides the control as argument of the callback.

Enhances

  • Add events like onChanged, onEditingComplete to reactive widgets.

Features

  • Add widget ReactiveFormConfig to globally define validation messages at Flutter application level. This reliefs the need to define validation messages in each reactive widget.

13.0.1

Enhances

  • Upgrade example folder project to Flutter 3.0.0.
  • Increase code coverage in FormArray.

13.0.0

Breaking Changes

  • Reactive Forms is now migrated to Flutter 3.0.0.

12.0.0

Fix

  • Update project to new Android wrappers in order to be able to run the example using latest Android SDK changes.

11.1.0

Enhances

  • Add Focus handling for several reactive widgets:
    • ReactiveSlider
    • ReactiveSwitch
    • ReactiveRadio
    • ReactiveRadioListTile
    • ReactiveCheckbox
    • ReactiveCheckboxListTile

11.0.2

Fix

  • Small fix in the array_sample example application.

11.0.1

Fix

  • Fix issues in ReactiveDatePicker when control value was before or after the range of firstDate and lastDate

Enhances

  • Add optional argument initialDate to the ReactiveDatePicker.

11.0.0

Breaking Changes

  • Reactive Forms is now migrated to Flutter 2.8.0.

Enhances

  • Update reactive widgets with extra properties presents in Flutter 2.8.0.

10.7.0

Features

  • Expose TextEditingController as a property of the ReactiveTextField for text selection purposes only.

10.6.8

Fix

  • Fix Async Validators that overrides validations errors from Sync Validators

10.6.7

Enhances

  • Improve Complex example in the demo application

10.6.6

Enhances

  • Improve the Array example in the demo application
  • Better exception handler when creating FormGroups with controls that contains character '.' in the name of the control.

10.6.5

Enhances

  • Add an example of deleting item from a FormArray in the example folder project.
  • Make public the previously private class InheritedStreamer

10.6.4

Fix

  • Minor documentation fix.

10.6.3

Enhances

  • Add documentation of new advanced reactive widgets.

Fix

  • Minor documentation fix.

10.6.2

Enhances

  • Converted ReactiveForm widget from Stateful to Stateless widget.

10.6.1

Enhances

  • Update the lint code analysis to use the las package lints.
  • Minor internal code checks and improvements.

10.6.0

Enhances

  • Added optional argument to Validators.mustMatch that allows to define if we want to mark the matchingControlName control as DIRTY when the control is invalid. See related issue.
  • Minor changes in model classes that allows to create custom AbstractControl implementations

10.5.0

Enhances

  • Added missing props pass through for widgets.

10.4.1

Enhances

  • Add minor internal changes for handling focus node in reactive text fields.

10.4.0

Fix

  • Add Flutter sdk version restriction in pubspec.yaml file.
  • Fix Validators.compare in release mode that has different behavior from debug mode.

10.3.0

Features

  • Add method in FormGroup that allows to remove a control by its name.

Enhances

  • Add missing extra properties to ReactiveRadioListTile and ReactiveRadio widgets.
  • Add a SliderIntValueAccessor that allows ReactiveSlider to bind to controls of type int.

10.2.0

Fix

  • Fix error when defining a ReactiveTextField with dynamic type and bound the widget with controls other than String.

Enhances

  • Add the optionally extra argument autoValidate to the method AbstractControl.setValidators to recalculate the validaity of the control after set the new validators without explicitly call updateValueAndValidity on that control.

10.1.0

Features

  • Add/Clear validators dynamically

Fix

  • Fix error when defining a ReactiveTextField without specifying a model data type

Enhances

  • Change the data type of ValidatorFunction to returns Map<String, dynamic> instead of Map<String, Object>

10.0.4

Fix

  • Fix exception when focus a control that does not exist in a group

Enhances

  • Better type definition in a control value accessor within a ReactiveFormField

10.0.3

Fix

  • Set ReactiveTextField maxLines as optional nullable argument.

10.0.2

Enhances

  • Add minor changes in Validators.min and Validators.max for handlig type checks
  • Add minor changes in ReactiveValueListenableBuilder for handlig type checks

10.0.1

Fix

  • Fix the error when defining a ReactiveFormField (related to control value accessor)

10.0.0

Breaking changes

  • Reactive Forms is now migrated to Flutter 2.x.
  • Reactive Forms is now Null-safety.
  • New Definitions in Custom Reactive Widgets. ReactiveFormField now defines the data type of the model (control) and the data type of the view (widget).
  • Validators.requiredTrue now has the 'requiredTrue' validation message and not 'requiredEquals' as in previous versions.

Features

  • Now you can specify a different validation message to the Validators.pattern validator.

9.1.0

Enhances

  • Add latest version of intl package

9.0.2

Fix

  • Remove Validators String type to fix casts errors when calling form.control('').validators in a control with a non dynamic type validator.

9.0.1

Fix

  • Add minor changes to successfully pass pub dev static analysis.
  • Add Library documentation.

9.0.0

Breaking changes

  • An enhanced strongly typed system to improve casting at compilation time and improve casting exception handler in runtime. This version was created to be compatible with analysis options in "implicit-casts: false" and "implicit-dynamic: false".

8.0.3

Fix

  • Code formatting issues

8.0.2

Fix

  • Validators.pattern now validates against a RegExp instance.

8.0.1

Fix

  • If a control is disabled, then it doesn't fire state change again when call markAsDisabled.

8.0.0

Breaking changes

  • Upgrade Reactive Forms to Flutter 1.20

Features

  • Add intl package and and optional argument to DateTimeValueAccessor to specify the format of the date.
  • Add some extra properties to reactive forms widgets.

7.6.3

Fix

  • Set cursor at the end of the text when set value to reactive text field from the FormControl.

7.6.2

Fix

  • Exposes ReactiveSwitchListTile.

7.6.1

Fix

  • Fix code formatting to pass pub static analysis.

7.6.0

Features

  • Expose focus controller of the FormControl. Now is possible to access UI FocusNode with in a control through control.focusController.focusNode.

7.5.0

Features

  • Add argument focusNode to ReactiveTextField to provide a custom FocusNode.

Enhances

  • Add some improvement to TimeOfDayValueAccessor.
  • Add some improvement to example application.

7.4.0

Enhances

  • A disabled ReactiveDropdownField now tale into account the selectedItemBuilder method to show the disabledHint.
  • More complete example app project inside Reactive Forms.

7.3.0

Features

  • Add FormGroup.rawValue and FormArray.rawValue to get the value of groups and array including any disabled controls.

7.2.1

Fixes

  • Fix exception when call FormGroup.addAll in a sub-group with parent.

7.2.0

Features

  • Add method patchValue to FormControl, FormGroup and FormArray to update partially the control value.

7.1.0

Features

  • Add Validators.any that requires any element of the control's iterable value satisfies a test function.

Fix

  • Update value accessors in reactive widgets when widget didUpdateWidget.

7.0.12

Enhances

  • Add readOnly argument as not nullable in ReactiveDropdownField.

7.0.11

Enhances

  • Add readOnly argument in ReactiveDropdownField constructor to enable/disable widget.

Documentation

  • Add documentation about customizing when to show errors in reactive widgets.

7.0.10

Fixes

  • Dispose FocusNode after FocusNodeController is disposed.

7.0.9

Documentation

  • Improves code documentation for control, group and array constructors.

7.0.8

Features

  • Add async validators to FormGroup and FormArray.
  • Add disabled optional argument to FormArrayConstructor.

7.0.7

Fixes

  • Fix now AbstractControl.removeError doesn't marks the control as dirty by default.

7.0.6

Fixes

  • Fix Validators.compare in DateTime controls type.

7.0.5

Features

  • Initialize a FormGroup as disabled is now possible with optional constructor argument.

7.0.4

Fixes

  • Fix Validators.maxLength and Validators.minLength when control value is null.

7.0.3

Features

  • Add new control value accessor Iso8601DateTimeValueAccessor that brings the possibility to bind a ReactiveDateTimePicker widget to a control of type String.

7.0.2

Fixes

  • Fix when the execution of asynchronous validators completes the control is not marked as dirty.

7.0.1

Features

  • Add new AbstractControl validator Validators.contains.

Fixes

  • Fix Validators.minLength and Validators.maxLength with FormControls of type dynamic when a value is a String.

7.0.0

Breaking changes

  • Change ReactiveFormField.validationMessages from a Map to a Function that receives the instance of the control and returns the Map with the customized validation messages. This upgrade now brings the possibility to dynamically change the validation messages based on the current error data.
  • Change Validators.email response error Map. Now returns the current control value instead of true.

Features

  • Add AbstractControl.hasError(...) for asking if a control has an error based on error code and children path.
  • Add AbstractControl.getError(...) to get error data based on error code and children path.

6.0.5

Fix

  • Fix error when trying to get a deep control within an array of groups.
  • Fix minor issues
  • Fix some typos.
  • Fix ReactiveDatePicker error when lastDate is previous to DateTime.now() and control value is null.

Enhanced

  • FormArray.removeAt now returns the removed control.
  • Improve code documentation.
  • Add more tests.
  • Add more strict data types in arguments.
  • Add extra arguments to AbstractControl.setErrors and AbstractControl.removeError to marks the control as dirty or pristine.
  • Refactor Validators.minLength and Validators.maxLength to be use with:
    • FormArray
    • FormControl of type Iterable
    • FormControl of type String
    • FormGroup
  • Add better data types definition in FormBuilder.array declaration.

6.0.4

Features

  • Add arguments emitEvent and updateParent to FormArray.clear() method.

6.0.3

Enhanced

  • Changes all arguments of data type Iterable with List data type to force compiler errors when not correctly cast MappedListIterable with List.

6.0.2

Fixes

  • Fix control value accessor that doesn't update the control when LengthLimitingTextInputFormatter reached the max length.

6.0.1

Fixes

  • Fix error when dispose a FocusController already registered in a control.

6.0.0

Breaking changes

  • Add big refactor in focus handlers of a FormControl. Change argument data type in FormControl.focusChanges event, from FocusEvent to bool.

Fixes

  • Fix infinity loop that freeze apps when two or more ReactiveTextField were binded to the same FormControl and changing focus between them.
  • Fix FormGroup.addAll now updates group pristine/dirty state of the group and trigger FormControlCollection.collectionChanges event.

Features

  • Add FormArray.clear() that remove all children controls of the array.

5.0.4

Features

  • Add optionally argument showErrors to ReactiveTextField and ReactiveDropdownField to customize when to show up validations messages. Validation messages by default change to visible when control is invalid and touched. With showErrors function this default behavior can be customized.

5.0.3

Breaking changes

  • Rename FormControl.focused by FormControl.hasFocus.
  • Change ReactiveFormBuilder.builder(context) by ReactiveFormBuilder.builder().
  • Change argument in FormControl.focusChanges event from bool to FocusEvent.

Features

  • Add FormGroup.unfocus() and FormArray.unfocus() to remove focus of children controls.
  • Add FormArray.focus(String name) to set focus on a control.
  • Add optional argument to AbstractControl.unfocus(bool touched) to mark controls as untouched when remove focus.
  • Add optional argument to AbstractControl.reset(bool removeFocus) to remove focus on control when reset the control/form/array.

Fixes

  • Fix ReactiveFormBuilder initializations in debug mode.

5.0.2

  • Fix add custom valueAccessor to text field.

5.0.1

  • Fix Validators.min and Validators.max with non comparable controls.

5.0.0

Breaking changes

  • Rename touch() and untouch() by markAsTouched() and markAsUntouched()
  • Rename enable() and disable() by markAsEnabled() and markAsDisabled()
  • Add AbstractControl.markAllAsTouched to mark all controls of a FromGroup or a FormArray as touched. AbstractControl.markAsTouched doesn't marks children as touched anymore.
  • Validators.compose now act as and AND and returns a ValidatorFunction instead of a List of ValidatorFunction
  • InputParsers have been replaced by ControlValueAccessor.

Features

  • Add new control status pristine and dirty.
  • FormGroup.control(String name) and FormArray.control(String name) now let specify the name argument as a dot-delimited string that represents the path to the nested control as nested1.nested2.nested.3.etc.
  • Add FormBuilder.array() and FormBuilder.control() for creating arrays and controls.
  • ReactiveValueListenableBuilder brings now the possibility to provide directly the control instead of just the control name.
  • Add Validators.composeOR to combines multiples validators in one and evaluates as an OR, if at least one validator evaluates to VALID then the control is valid.
  • Add ControlValueAccessor to convert value data types from UI to model and vice versa.
  • Add FormGroup.focus(String name) to set focus to a child control.

Widgets

  • Add ReactiveFormBuilder useful for defining a FormGroup and a ReactiveForm at the same time in a stateless widget.

Validators

  • Add validators:
    • Validators.requiredTrue
    • Validators.equals
    • Validators.min
    • Validators.max
    • Validators.composeOR
    • Validators.compare

Fixes

  • ReactiveRadioListTile widget is now available.

4.0.2

  • Fix FormBuilder.group() with initial value in null.

Features

  • Add FormBuilder.state to create a ControlState.

4.0.1

  • Fix async update in InheritedStreamer widget that raised exception when declare a form group in a stateless widget.

4.0.0

Features

  • FormControl.reset() can now set the disabled status of the control as an optional argument.
  • Implements FormGroup.resetState() and FormArray.resetState() to reset children controls with initial value and disabled status.

Breaking Changes

  • FormControl constructor receives value instead of defaultValue. To reset a control to a initial value you must call FormControl.reset() and supply initial value.

3.0.1

  • Fix Luhn algorithm in credit card validator.

3.0.0

  • Fixes:

    • FormGroup.reset() marks control as untouched and hide UI validation errors.
    • FormGroup.reset() sets empty string to input text fields if control value is null.
  • Features:

    • InputParser added to ReactiveTextField. Now you can declare a FormControl with int or double data type and bind it to an input text field.

2.0.8

  • Fixed card number length in Validators.creditCard

2.0.7

  • Minor changes and code documentation.
  • New Validator
    • *Validators.creditCard* that validates a credit card number using the Luhn algorithm.

2.0.6

  • Optimized FormGroup and FormArray value gets.
  • FormBuilder docs added to README.md

2.0.5

  • fixed ReactiveDropdownField.onChanged not triggered when control value didn't changes

2.0.4

  • Minor fixes in ReactiveDropdownField.

2.0.3

  • Minor fixes.

2.0.2

  • Added basic implementation of FormBuilder to build FormGroup easily.
  • Added onChanged callback to ReactiveDropdownField.

2.0.1

  • Now ReactiveDropdownField not set value of control to null when no matching item founded.

2.0.0

  • Added onWillPop to ReactiveForm widget.
  • Enable/disable controls, groups and arrays.
  • Now you can access the parent group or array with the property FormControl.parent.

Fixes

  • Removed restriction of empty item's array in ReactiveDropdown.
  • Assert error in ReactiveDropdown when control value not match any item values.
  • Slider assert error when control bounded to slider initialize in null value.

Breaking Changes

  • Use of Streams to notify events.
  • Renamed event names.

See Migration Guide to see how to migrate from version 1.x to 2.x.

1.1.0

  • Async validators now have a debounce timer in milliseconds that can be passed as argument to FormControl. This is useful for example to reduce requests to an API.
  • Now you can optionally pass a FormControl directly to reactive widgets instead of the control's name. You must provide a formControlName or a formControl but not both at the same time.
  • Set value an array value to FormArray insert missing items

1.0.10

  • Composing validators with Validators.compose
  • Added optionally type to ReactiveFormArray widget

1.0.9

  • Added utilities an extensions to AbstractControl
    • AbstractControl.isNull
    • AbstractControl.isNullOrEmpty for controls of type String
  • Minor fixes

1.0.8

Fixes

  • FormArray and FormGroup now notify value changes when a control without validators changes its value.

1.0.7

  • FormGroup.touch mask all controls as touched.

  • Renamed FormGroup.formControl to FormGroup.control and FormArray.formControl to FormArray.control to get controls by name.

  • ReactiveValueListenableBuilder.builder now pass the control as parameter instead of the value so you can get access to the control within the builder function.

  • Added FormGroup.controls and FormArray.controls to iterate over child controls.

  • Bonus widgets:

    • ReactiveDatePicker
    • ReactiveTimePicker

1.0.6

  • Fixed ReactiveCheckbox and ReactiveCheckboxListTile exception when binding with FormControl with null value.
  • Increased code tests coverage to 92% (a lot of tests).
  • FormArray.removeAt method added to remove a control given the index position.

1.0.5

  • Added ReactiveTextField.onSubmitted for an example of how to handle moving to the next/previous field when using TextInputAction.next and TextInputAction.previous for textInputAction.
  • Included property ReactiveSwitchListTile.inactiveThumbImage.
  • Included properties autofocus to ReactiveCheckbox, ReactiveRadio and ReactiveSwitch.
  • Included callbacks ReactiveSlider.onChangeEnd and ReactiveSlider.onChangeStart.

1.0.4

Fixes:

  • FormArray now correctly notify status depending of children status.

1.0.3

New Validators

  • Async Validators (only FormControl for now...)
  • Controls have now three different states: VALID, INVALID, PENDING (this last one was specially included due to async validators, the control is PENDING until validator completes)
  • Added documentations about Validator.pattern
  • Added Async Validator's example in example application /example/main.dart.

New Models

  • FormArray (aggregates the values of each child FormControl into an array)

New Reactive Widgets:

  • ReactiveFormArray
  • ReactiveCheckboxListTile
  • ReactiveSwitchListTile
  • ReactiveRadioListTile
  • ReactiveStatusListenableBuilder

1.0.2

  • Added class ValidationMessage for common validation messages key as: required, email, etc.
  • Added Documentation of ValidationMessage.
  • Minor typo fix in documentation text.

1.0.1

  • Added an example application

1.0.0

Predefined validators

  • Validators.required
  • Validators.email
  • Validators.number
  • Validators.minLength
  • Validators.maxLength
  • Validators.pattern

Supported Reactive Form Fields Widgets

  • ReactiveTextField
  • ReactiveDropdownField
  • ReactiveSwitch
  • ReactiveCheckbox
  • ReactiveRadio
  • ReactiveSlider

Other Reactive Forms Widgets

  • ReactiveForm
  • ReactiveFormConsumer
  • ReactiveValueListenableBuilder