-
Notifications
You must be signed in to change notification settings - Fork 89
Validators (Asynchronous)
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 the email is unique in your System. We will use ReactiveTextField to introduce the email, and a Button that will get enabled/disabled depending of the state of the Form.
First let's declare our form:
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 = ['johndoe@email.com', 'john@email.com'];
/// 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 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) {
// force validation message to show up as soon as possible
control.touch();
return error;
}
return null;
}
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.
Ok now let put some context and let's see how we can build a UI for this example:
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// We don't recommend the use of ReactiveForms in this way.
// We highly recommend using the Provider plugin
// or any other state management library.
//
// We have declared the FormGroup within a Stateful Widget only for
// demonstration purposes and to simplify the explanation in this documentation.
final form = FormGroup({
'email': FormControl<String>(
validators: [
Validators.required, // traditional required and email validators
Validators.email,
],
asyncValidators: [
_uniqueEmail // custom asynchronous validator :)
],
),
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'email',
decoration: InputDecoration(
labelText: 'Email',
),
validationMessages: {
ValidationMessage.required: 'The email must not be empty',
ValidationMessage.email: 'The email value must be a valid email',
'unique': 'This email is already in use',
},
),
ReactiveFormConsumer(
builder: (context, form, child) {
return RaisedButton(
child: Text('Sign Up'),
onPressed: form.valid ? () {} : null,
);
},
),
],
),
),
),
);
}
}
/// just a simple array to simulate a database of emails in a server
const inUseEmails = ['johndoe@email.com', 'john@email.com'];
/// 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 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) {
// force validation message to show up as soon as possible
control.touch();
return error;
}
return null;
}
Quite simple isn't it? We have just add a ReactiveTextField binded to the email FormControl and a RaisedButton that get enabled/disabled depending if the FormGroup is VALID or not. We have wrap the RaisedButton in a ReactiveFormConsumer to listen when the status of the FormGroup changes and rebuild the button to reflect that status.
Ok we have implemented the async validation but it would be nice if we can show some feedback to the user that something is happening in background.
Let's show a simple CircularProgressIndicator inside the text field while the async validator is running. We will show the progress indicator only when the email FormControl change its status to PENDING because the asyn validation is running.
With ReactiveStatusListenableBuilder widget 😁
ReactiveStatusListenableBuilder will listen to chages in the status of a FormControl and rebuild the context of child widget each time the status changes. Let's put a ReactiveStatusListenableBuilder as a suffix of the text field and render a CircularProgressIndicator is the email status is PENDING:
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// We don't recommend the use of ReactiveForms in this way.
// We highly recommend using the Provider plugin
// or any other state management library.
//
// We have declared the FormGroup within a Stateful Widget only for
// demonstration purposes and to simplify the explanation in this documentation.
final form = FormGroup({
'email': FormControl<String>(
validators: [
Validators.required, // traditional required and email validators
Validators.email,
],
asyncValidators: [
_uniqueEmail // custom asynchronous validator :)
],
touched: true, // not necessary, it's just to see error messages as soon as possible
),
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Async validators example'),
),
body: Center(
child: ReactiveForm(
formGroup: this.form,
child: Column(
children: <Widget>[
ReactiveTextField(
formControlName: 'email',
decoration: InputDecoration(
labelText: 'Email',
suffixIcon: ReactiveStatusListenableBuilder( // listen status changes of email field
formControlName: 'email',
builder: (context, control, child) {
return control.pending
? CircularProgressIndicator() // progress indicator if status is 'pending'
: Container(width: 0);
},
),
),
validationMessages: {
ValidationMessage.required: 'The email must not be empty',
ValidationMessage.email:
'The email value must be a valid email',
'unique': 'This email is already in use',
},
),
ReactiveFormConsumer(
builder: (context, form, child) {
return RaisedButton(
child: Text('Sign Up'),
onPressed: form.valid ? () {} : null,
);
},
),
],
),
),
),
);
}
}
/// just a simple array to simulate a database of emails in a server
const inUseEmails = ['johndoe@email.com', 'john@email.com'];
/// 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 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) {
// force validation message to show up as soon as possible
control.touch();
return error;
}
return null;
}
And that's it!!!!
Even though you don't visually see a pretty UI (just because I haven't styled or implemented a visual theme, remember that the Reactive Forms widgets have all the customization capabilities that the native Flutter widgets has) you can't deny me that the code has been very clean, beautiful and easy to understand. 😉