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

Overview

Reactive Forms

This is a model-driven approach to handling Forms inputs and validations, heavily inspired in 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: >=2.12.0 <3.0.0
  • Flutter: >= 2.2.0

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: ^10.6.6

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

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

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

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

But you can also get the complete Form data as follows:

print(form.value);

The previous code prints the following output:

{
  "name": "John Doe",
  "email": "[email protected]"
}

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

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

A custom FormControl validator is a function that receives the control to validate and returns a Map. If the value of the control is valid the function must returns null otherwise returns a Map with a key and custom information, in the previous example we just set true as custom information.

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

final form = FormGroup({
  'acceptLicense': FormControl<bool>(
    value: false, 
    validators: [_requiredTrue], // custom validator
  ),
});
/// 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};
}

You can see the current implementation of predefined validators in the source code to see more examples.

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

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 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 that cases we could implement a custom validator 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: [
  _mustMatch('password', 'passwordConfirmation')
]);

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:

ValidatorFunction _mustMatch(String controlName, String matchingControlName) {
  return (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 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'),
]);

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 it 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: [_uniqueEmail], // custom asynchronous validator :)
  ),
});

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:

/// just a simple array to simulate a database of emails in a server
const inUseEmails = ['[email protected]', '[email protected]'];

/// Async validator example that simulates a request to a server
/// and validates if the email of the user is unique.
Future<Map<String, dynamic>> _uniqueEmail(AbstractControl<dynamic> control) async {
  final error = {'unique': false};

  final emailAlreadyUsed = await Future.delayed(
    Duration(seconds: 5), // a delay to simulate a time consuming operation
    () => inUseEmails.contains(control.value),
  );

  if (emailAlreadyUsed) {
    control.markAsTouched();
    return error;
  }

  return null;
}

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

The previous implementation was a simple function 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.

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(
  asyncValidators: [_uniqueEmail],
  asyncValidatorsDebounceTime: 1000, // sets 1 second of 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 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 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),
      ])
    ],
  ),
});

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

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

You can collect all data using FormGroup.value:

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

The previous method outputs a Map as the following one:

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

And of course you can access to a nested FormGroup as following:

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

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

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);
{
  "emails": ["[email protected]", "[email protected]"]
}

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

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]");

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(),
);
// 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 follow:

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

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(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 default value and disabled state
  'name': fb.state(value: 'john', disabled: true),
);

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

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

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

How to customize error messages?

@override
Widget build(BuildContext context) {
  return ReactiveForm(
    formGroup: this.form,
    child: Column(
      children: <Widget>[
        ReactiveTextField(
          formControlName: 'name',
          validationMessages: (control) => {
            'required': 'The name must not be empty'
          },
        ),
        ReactiveTextField(
          formControlName: 'email',
          validationMessages: (control) => {
            'required': 'The email must not be empty',
            'email': 'The email value must be a valid email'
          },
        ),
        ReactiveTextField(
          formControlName: 'password',
          obscureText: true,
          validationMessages: (control) => {
            'required': 'The password must not be empty',
            'minLenght': 'The password must have at least 8 characters'
          },
        ),
      ],
    ),
  );
}

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: (control) => {
     ValidationMessage.required: 'The email must not be empty',
     ValidationMessage.email: 'The email value must be a valid email',
   },
),

nice isn't it? ;)

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(
    value: 'John Doe',
    validators: [Validators.required],
    touched: true,
  ),
});

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
}

To mark all children controls of a FormGroup and FormArray you must call markAllAsTouched().

final form = FormGroup({
  'name': FormControl(
    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 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,
),

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:

  1. Separating Submit Button in a different Widget.
  2. 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(), 
      ],
    ),
  );
}

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

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

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

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

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

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

// the control is disabled and also the widget in UI is 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 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',
          ),
        ),
      ],
    ),
  );
}

Because of the two-binding capability of the ReactiveTextField with a FormControl the widget don't include properties as controller, validator, autovalidate, onSaved, onChanged, onEditingComplete, onFieldSubmitted, the FormControl is responsible for handling validation as well as changes notifications.

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.

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

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

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

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

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.

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.

Comments
  • FormControl not updating with different control than previous build

    FormControl not updating with different control than previous build

    Hi. I have a form that shows some field and when data saved, I update entire FormGroup to a new Form and also all FormControl will updated as well. but the control in the ui not updated and it uses the old FormControl.

    Is this intended or a bug?

    After that I just modified ReactiveTextField and ReactiveFormField to change controller in didUpdateWidget and everything work perfectly.

    you can see modified code

    If looks good I can make a PR

    opened by rebaz94 37
  • Impossible to write into an input text field (virtual keyboard goes away on focus to the field)

    Impossible to write into an input text field (virtual keyboard goes away on focus to the field)

    Hello, I come back here with a new problem. When a user is connected, my application displays a navigation bar with a page containing a ReactiveForm. But when i click on an input text field:

    • the phone keyboard start to be displayed
    • the navigation bar stay above it
    • and then the keyboard goes away so I can't write anything in the input text field

    In the debug console I have these messages:

    W/IInputConnectionWrapper( 4350): getTextBeforeCursor on inactive InputConnection
    W/IInputConnectionWrapper( 4350): getSelectedText on inactive InputConnection
    W/IInputConnectionWrapper( 4350): getTextAfterCursor on inactive InputConnection
    

    Before to be connected I have another ReactiveForm but without a navigation bar so I don't have any problem with the keyboard. I tried a lot of things but I can't find a way to correct this behavior.

    My configuration:

    ✗ flutter doctor
    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 2.0.6, on macOS 11.2.1 20D74 darwin-arm, locale fr-FR)
    [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    [✓] Xcode - develop for iOS and macOS
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 4.1)
    [✓] Connected device (2 available)
    
    • No issues found!
    
    help wanted 
    opened by jbt-cii 35
  • Form generator and package roadmap

    Form generator and package roadmap

    Hi @joanpablo I have an idea to write generator which will help to overcome some disadvantages of JSON access to field values described in this issue https://github.com/joanpablo/reactive_forms/issues/28

    Something like

    @ReactiveForm
    User() {
    
    }
    

    Also I would like to discuss with you if there is a possibility to create some roadmap about form engine features to implement)

    If you are ok with that ping me in discord vasilich6107#1453

    opened by vasilich6107 28
  • Expose focus node of text field

    Expose focus node of text field

    Hello again, I'm trying to add a keyboard action bar with this package. The package is demanding a focus node to know when the text field is focused or not to show the action bar of the keyboard. I don't think there is a way to get the focus node from the formControl associated with the text field. And there is no way to add a custom focus node to the reactive text field either. Please advise, Thank you

    enhancement 
    opened by naamapps 26
  • Null-Safety with 9.0.0 changes

    Null-Safety with 9.0.0 changes

    Hi I work with Greg and did requested changes from 9.0.0 branch. I also resolved that one failing tests we had before. Please review it and try to merge it at least to develop branch.

    opened by radvansky-tomas 25
  • Dynamic list of ReactiveTextField does not update the value when an item is removed

    Dynamic list of ReactiveTextField does not update the value when an item is removed

    Hello!

    I am trying to implement a relatively simple behavior, a dynamic list of strings. In this list I can add new elements dynamically and delete any item from the list. However I am having a problem, when I delete an item other than the last one I am observing strange behavior. The values ​​are not updated on the screen despite making sure that the right form control and list item is cleared. So when I click to remove an item from the middle of the list, it gives the impression that it removed the last one but it removed the correct one, only the values ​​of the screen that have not been updated

    Am I doing something wrong or could this be a bug?

    pubspec.yaml
    name: app
    description: A new Flutter project.
    publish_to: 'none' # Remove this line if you wish to publish to pub.dev
    version: 1.0.0+1
    
    environment:
      sdk: ">=2.12.0 <3.0.0"
    
    dependencies:
      flutter:
        sdk: flutter
      reactive_forms: 10.3.0
      get: 4.1.4
    
    dev_dependencies:
      flutter_test:
        sdk: flutter
    
    # The following section is specific to Flutter.
    flutter:
      uses-material-design: true
    
    
    example
    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    import 'package:reactive_forms/reactive_forms.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        Get.put(HomeController());
        return GetMaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(),
        );
      }
    }
    
    class MyString {
      String s;
      MyString({
        required this.s,
      });
    }
    
    class HomeController extends GetxController {
      var myList = <MyString>[].obs;
    
      var formGroup = fb.group({'list': fb.array([])});
    
      FormArray get formArray => formGroup.control('list') as FormArray;
    
      void remove(MyString s) {
        var i = myList.indexOf(s);
    
        // this remove the right item but the valeu is't updated
        formArray.removeAt(i);
    
        // this disable the right item and update the right item
        //formArray.control(i.toString()).markAsDisabled();
    
        myList.remove(s);
      }
    
      void add(MyString s) {
        formArray.add(FormControl<String>(value: s.s));
        myList.add(s);
      }
    }
    
    class MyHomePage extends GetView<HomeController> {
      List<Widget> get list {
        return controller.myList.map((e) => formText(e)).toList();
      }
    
      Widget formText(MyString s) {
        var i = controller.myList.indexOf(s);
        return Container(
          child: Row(
            children: [
              Expanded(
                child: ReactiveTextField(
                  formControlName: 'list.$i',
                ),
              ),
              Expanded(
                  child: ElevatedButton(
                      onPressed: () {
                        controller.remove(s);
                      },
                      child: Text('-')))
            ],
          ),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: Obx(
          () => ReactiveForm(
            formGroup: controller.formGroup,
            child: Center(
              child: Column(
                children: [
                  ...list,
                  ElevatedButton(
                      onPressed: () {
                        controller
                            .add(MyString(s: Random().nextInt(100).toString()));
                      },
                      child: Text('+'))
                ],
              ),
            ),
          ),
        ));
      }
    }
    
    
    flutter doctor
    [✓] Flutter (Channel stable, 2.2.0, on macOS 11.1 20C69 darwin-x64, locale
        pt-BR)
    [✓] Android toolchain - develop for Android devices (Android SDK version
        30.0.3)
    [✓] Xcode - develop for iOS and macOS
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 4.1)
    [✓] VS Code (version 1.56.2)
    [✓] Connected device (1 available)
    
    opened by rooque 19
  • ReactiveDropdownField not showing Text with Release Web Build

    ReactiveDropdownField not showing Text with Release Web Build

    Having a ReactiveDropDownField like the following the release web build doesn't display the Text in the dropdown. Additionally the label is cut off on the element.

     ReactiveDropdownField<int>(
                        formControlName: 'locationSpotId',
                        //hint: Text('Spot Number'),
                        decoration: InputDecoration(
                            border: OutlineInputBorder(),
                            //contentPadding: EdgeInsets.fromLTRB(0, 5.5, 0, 0),
                            labelStyle: TextStyle(),
                            labelText: 'Parking Spot #'),
                        items: widget.data.location.locationSpots
                            .map((x) => DropdownMenuItem(child: Text(x.name), value: x.id))
                            .toList()),
    

    Here is a screencast showing both. The first part of the video shows the form built in release mode and the 2nd hafl of the video in profile mode.

    https://www.dropbox.com/s/npubiunoto34pgq/2020-10-06_16-31-57.MP4?dl=0

    opened by sbosell 19
  • [Feature Request] Suppress error

    [Feature Request] Suppress error

    Hi,

    I would like all my forms to have validation only on submission (#205), but it would be too tedious to override the showError function for each control.

    Can we have a suppress error method to not show the error? My idea is to instantiate the FormControl with suppress error enabled, upon form submission, will reinstate the error (suppress error disabled) for all controls.

    With this simple change, it really helps a lot!

    Thanks in advance.

    opened by jasonlaw 18
  • Allow specifying a custom `initialTime` for ReactiveDatePicker

    Allow specifying a custom `initialTime` for ReactiveDatePicker

    I have a use case where the firstDate depends on the current date. The property is stored in a database, but is editable. This means that I end up with a case where a user selected a date that was valid at the time they selected it. However, if the then selected date has passed, the user is no longer able to edit the date, to set a new, valid date.

    The specific error is something like: 'package:flutter/src/material/date_picker.dart': Failed assertion: line 167 pos 5: '!initialDate.isBefore(firstDate)': initialDate 2022-01-26 00:00:00.000 must be on or after firstDate 2022-01-27 00:00:00.000.

    The issue is obvious, the date is set to a past date, which is no longer valid. But the user should still be able to edit it. Currently, the only solution is to modify the Form data in order to update the value to a valid date, before opening the picker. However this is causes a whole range of other issues and problems.

    It seems that an obvious and simple fix would be to let us specify a custom nullable initialDate, which would take precedence over any other date when provided.

    opened by ezet 17
  • Being able to copyWith a ReactiveTextField OR expose the fields of ReactiveTextField

    Being able to copyWith a ReactiveTextField OR expose the fields of ReactiveTextField

    Hi, I implemented a widget called AutoDirectionTextField which uses ReactiveValueListenableBuilder to update the text direction when the text of the field updates according to the language the user typed. The problem is that it is not manageable because with each update there are some breaking changes and new parameters to the ReactiveTextField. There are two options to tackle this:

    1. Extend ReactiveTextField - but the fields are not exposed because they are passed directly to the constructor.
    2. Pass ReactiveTextField to my widget - but there is no option to change the text direction also because the fields are not exposed.

    The solution would be to expose the fields or add a copyWith method to allow us to copy an existing ReactiveTextField and override them if needed.

    Please help, Thanks

    opened by naamapps 17
  • validators proposal

    validators proposal

    Hi @joanpablo I have a proposal regarding refactoring of validators approach which could possibly solve the https://github.com/joanpablo/reactive_forms/issues/168 and make validators usage more consistent.

    My initial idea is to get rid from callbacks and improve the Validator abstract class to handle all in one

    • validation function
    • key
    • message

    look here https://github.com/joanpablo/reactive_forms/compare/master...artflutter:validators-proposal?expand=1#diff-18a9b38eff05410c316cb7546fade4248de3a3ada1ed2f0862dbf5d6cfb236e8L8

    This will give us an ability to define validation messages at the validator level like https://github.com/joanpablo/reactive_forms/compare/master...artflutter:validators-proposal?expand=1#diff-c97e7828e7b481d3fa831aceeba02c4c3ffc0bce2493d766907d2d2496c60071R9

    And also override them on control level if needed like https://github.com/joanpablo/reactive_forms/compare/master...artflutter:validators-proposal?expand=1#diff-c97e7828e7b481d3fa831aceeba02c4c3ffc0bce2493d766907d2d2496c60071R14

    This approach will also give another level of consistency for the code generator.

    I will be able to generate classes for listing error messages to return from validationMessages fn. something like if you set a RequiredValidator for email fields - the class EmailValidationMessages will generated.

    class EmailValidationMessages {
      final String? reqiredMessage;
    
      EmailValidationMessages({this.reqiredMessage});
    
      Map<String, String> messages => {
        'required': reqiredMessage,
      }
    }
    

    Let's summarise.

    After proposed refactoring we will be able to

    • specify common error messages at validator level requested in https://github.com/joanpablo/reactive_forms/issues/168
    • ability to override the message at control level
    • allow generator to generate more helpful code to help definig error messages in predicatable way
    opened by vasilich6107 16
  • MustMatchValidator marks the matchingControl as touched

    MustMatchValidator marks the matchingControl as touched

    @joanpablo Hi, I got same problem with #199

    Now I am using V14.1.0 It works as before. Can you check it, please? validators: [ Validators.mustMatch('password', 'passwordConfirmation', markAsDirty: false), ]

    opened by byungjuJin 0
  • Unable to implement FormArray having FormGroups

    Unable to implement FormArray having FormGroups

    I am trying to implement a form having a FormArray of FormGroups. However, I am unable to get it working. I could not find a working implementation for the same, in the package examples or the Internet. The problem is with writing Widgets for the FormArray part. I have tried using several approaches, but no success.

    Here is the sample code that uses two approaches using formControl and formControlName in a ReactiveTextField. While using formControl, the casting gives the error in

    formControl: control as FormControl<String>,
    

    Whereas, in formControlName approach, specifying the name as in qualif.$i.degree gives a problem (where $i refers to index as suggested in the array example).

    Please suggest where I am making mistakes or provide a link to a working example.

    The code is given here.

    import 'package:flutter/material.dart';
    import 'package:reactive_forms/reactive_forms.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'FormArray Demo'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: PersonFormView(personFormGroup));
      }
    }
    
    class Qualif {
      final String degree;
      final int year;
      const Qualif(this.degree, this.year, {Key? key});
    }
    
    class Person {
      final String name;
      final int age;
      final List<Qualif> qualif;
    
      const Person(this.name, this.age, this.qualif, {Key? key});
    }
    
    FormGroup personFormGroup = FormGroup({
      'name': FormControl<String>(),
      'age': FormControl<int>(),
      'qualif': FormArray([
        FormGroup({
          'degree': FormControl<String>(value: 'BE'),
          'year': FormControl<int>(value: 2000),
        })
      ]),
    });
    
    List<Widget> personFormFields = [
      ReactiveTextField(
        formControlName: 'name',
        decoration: const InputDecoration(
          label: Text('Name'),
        ),
      ),
      ReactiveTextField(
        formControlName: 'age',
        decoration: const InputDecoration(
          label: Text('Age'),
        ),
      ),
    
      // --------- using formControl ----------
      ReactiveFormArray(
        formArrayName: 'qualif',
        builder: (context, array, child) => Column(
          children: [
            for (final control in array.controls)
              Column(
                children: [
                  ReactiveTextField(
                    formControl: control as FormControl<String>,    // <== ERROR HERE
                    decoration: const InputDecoration(
                      label: Text('Degree'),
                    ),
                  ),
                  ReactiveTextField(
                    formControl: control,
                    decoration: const InputDecoration(
                      label: Text('Year'),
                    ),
                  ),
                ],
              ),
          ],
        ),
      ),
    
      // // --------- using formControlName ---------
      // ReactiveFormArray(
      //   formArrayName: 'qualif',
      //   builder: (context, array, child) => Column(
      //     children: [
      //       for (int i = 0; i < array.controls.length; i++)
      //         Column(
      //           children: [
      //             ReactiveTextField(
      //               formControlName: 'qualif.$i.degree',     // <== ERROR HERE
      //               decoration: const InputDecoration(
      //                 label: Text('Degree'),
      //               ),
      //             ),
      //             ReactiveTextField(
      //               formControlName: 'qualif.$i.year',
      //               // formControl: control,
      //               decoration: const InputDecoration(
      //                 label: Text('Year'),
      //               ),
      //             ),
      //           ],
      //         ),
      //     ],
      //   ),
      // ),
    ];
    
    class PersonFormView extends StatelessWidget {
      final FormGroup form;
    
      const PersonFormView(this.form, {super.key});
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: ReactiveForm(
            formGroup: form,
            child: Column(
              children: <Widget>[
                ...personFormFields,
              ],
            ),
          ),
        );
      }
    }
    
    opened by rsbichkar 7
  • Please update the intl package to v0.18.0

    Please update the intl package to v0.18.0

    Please update the intl package to v0.18.0 so we can avoid this error on resolving dependencies:

    Because reactive_forms >=9.1.0 depends on intl ^0.17.0

    https://pub.dev/packages/intl/changelog

    opened by chunghha 0
  • Proposal to make the value of a FormControl non-null

    Proposal to make the value of a FormControl non-null

    Currently when defining a FormControl we usually give it a Type. This type is then made nullable internally in the FormControl. The problem with this approach is that we might not want that behaviour.

    A solution (breaking change) would be to require the value parameter in the FormControl constructor. This would still allow nullable values by defining a nullable type parameter and then passing null as the initial value therefore not taking any features away.

    I've actually been faced with the same decision when migrating flutter_hooks to null-safety. The useState hook works in this fashion and I feel like FormControls should work the same way.

    Please let me know your opinion on this matter!

    opened by DevNico 6
  • ReactiveValueListenableBuilder issue

    ReactiveValueListenableBuilder issue

    Is there a builder that trigger updates when the touched status change or when the value change?

    Also is there a builder that can listen to changes of two or more input at the same time, instead of nesting the builder like the following?

    ReactiveValueListenableBuilder<bool>(
      formControlName: 'accept_tnc',
      builder: (context, tncControl, child) {
        return ReactiveValueListenableBuilder<bool>(
          formControlName: 'accept_privacy_policy',
          builder: (context, privacyControl, child) {
            final acceptTnc = tncControl.value ?? false;
            final acceptPrivacy =
                privacyControl.value ?? false;
            final touched = privacyControl.touched ||
                privacyControl.touched;
            print('tnc touched $touched');
            if ((!acceptTnc || !acceptPrivacy) &&
                touched) {
              return const MyText(
                text:
                    "You must read and agree with the Terms and Conditions and Personal Information Collection Statement to proceed your registration.",
                textStyle: TextStyle(
                  color: Colors.red,
                  fontSize: 10,
                ),
              );
            } else {
              return const SizedBox();
            }
          },
        );
      },
    )
    
    opened by danielchan303 3
Owner
Joan Pablo
Joan Pablo
A Widget that passes a Reactive Model to all of it's children

scoped_model A set of utilities that allow you to easily pass a data Model from a parent Widget down to its descendants. In addition, it also rebuilds

Brian Egan 779 Nov 30, 2022
-UNDER DEVELOPMENT- a project built demonstrating model view view model architecture

mvvm_project A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if

Atuoha Anthony 4 Nov 28, 2022
Flutter Architecture inspired by Domain Driven Design, Onion and Clean Architecture

Inspiring Domain Driven Design Flutter Architecture Please take a look at my slides to learn more Strategic Domain Driven Design For Improving Flutter

Majid Hajian 324 Dec 25, 2022
A flutter app which calculate your heart beat and takes few inputs and predict whether you have heart disease or not.

Heart Care App ?? ?? ❤️ ⭐️ Features: ⚡️ Lets you sign up and login ⚡️ Montioring your heart failure risk and heart beat through charts ⚡️ View Medical

HINA KHADIM 8 Oct 31, 2022
Vid formfield chipinput - Flutter custom formfield with Chip inputs. (Part of video code-along)

ex2_formfield A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if

Khalid Ali 0 Jan 1, 2022
User auth form - Signup and signin user auth form with ability to stay signed in and have an option to signout.

user_auth_form SIgnup and signin user authentification form Getting Started This project is a starting point for a Flutter application. A few resource

null 0 Jan 6, 2022
A Flutter package that provides a dropdown form field using a dropdown button inside a form field.

Dropdown form field A dropdown form field using a dropdown button inside a form field. Demo Features Can be used as regular form field. Simple to impl

Carlos Eugenio Torres 72 Jan 1, 2023
Form builder image picker - Form builder image picker for flutter

form_builder_image_picker Field for picking image(s) from Gallery or Camera for

Ferri Sutanto 0 Jan 28, 2022
Sample Flutter app for creating basic login forms validation for email and passwords

Email validation Flutter example Sample flutter app showing how to validate a e-mail login form. This example uses the email_validator package for val

Fredrik Eilertsen 22 Dec 15, 2022
Forms in Flutter without hassle!

Super Form Quick, familiar and extensible forms in Flutter ?? No magical configuration required ?? Managing form state with standard Flutter forms can

Bartosz Wiśniewski 11 Oct 24, 2022
Flutter App Templete is a project that introduces an approach to architecture and project structure for developing Flutter apps.

Flutter App Template "Flutter App Template" is a project that introduces an approach to architecture and project structure for developing Flutter apps

Altive 126 Jan 5, 2023
Flutter Control is complex library to maintain App and State management. Library merges multiple functionality under one hood. This approach helps to tidily bound separated logic into complex solution.

Flutter Control is complex library to maintain App and State management. Library merges multiple functionality under one hood. This approach helps to

Roman Hornak 23 Feb 23, 2022
EasiestSqlFlutter - The Easiest and the Laziest approach to Flutter SQL Database.

The Easiest and the Laziest approach to Flutter SQL Database for Flutter. • How to use • Contribution • License • Support • Share Sharing with your fr

Fayaz Bin Salam 15 Jul 27, 2022
Yet another localization approach in Flutter

Flutter Global Summit Vol.2 schedule Source code of the mobile application that displays the schedule of the Flutter Global Summit Vol.2 conference th

Anna Leushchenko 3 Mar 24, 2022
Clean Architecture Project with TDD Approach

Clean-Architecture-Project-with-TDD-Approach Small project to implement TDD(Testing Driven Development) by applying SOLID and YAGNI and rules(Clean Ar

null 7 Jun 24, 2022
A Flutter application with proper navigation and routes handling and API data fetching and posting.

Flutter-Navigation-and-API-Integration A Flutter application with proper navigation and routes handling and API data fetching and posting. ⏮ Preview G

Ehmad Saeed⚡ 7 Oct 5, 2022
App HTTP Client is a wrapper around the HTTP library Dio to make network requests and error handling simpler, more predictable, and less verbose.

App HTTP Client App HTTP Client is a wrapper around the HTTP library Dio to make network requests and error handling simpler, more predictable, and le

Joanna May 44 Nov 1, 2022
Learn how to build a tensorflow model on Techable Machine and then run it on flutter app.

Ml With Flutter Learn how to build a tensorflow model on Techable Machine and then run it on flutter app. Youtube Tutorial Show Support Recommend Me O

Sanskar Tiwari 133 Jan 3, 2023
Nexus is a state management library that makes it easy to create and consume your application's reactive data to the user interface.

Nexus ?? Nexus is a state management library that makes it easy to create and consume your application's reactive data to the user interface. With nex

Gor Mkhitaryan 3 Sep 7, 2022