Introduce new Form validation method (#135578)
Introduced `validateGranually` which, apart from announcing the errors to the UI, returns a `Map<Key, bool>` providing more granular validation details: The results of calling `validate` on each `FormField` and their corresponding widget keys. * related issue: #135363
This commit is contained in:
parent
536de5ed91
commit
0c40f21fc5
@ -291,18 +291,45 @@ class FormState extends State<Form> {
|
||||
/// returns true if there are no errors.
|
||||
///
|
||||
/// The form will rebuild to report the results.
|
||||
///
|
||||
/// See also:
|
||||
/// * [validateGranularly], which also validates descendant [FormField]s,
|
||||
/// but instead returns a [Set] of fields with errors.
|
||||
bool validate() {
|
||||
_hasInteractedByUser = true;
|
||||
_forceRebuild();
|
||||
return _validate();
|
||||
}
|
||||
|
||||
bool _validate() {
|
||||
|
||||
/// Validates every [FormField] that is a descendant of this [Form], and
|
||||
/// returns a [Set] of [FormFieldState] of the invalid field(s) only, if any.
|
||||
///
|
||||
/// This method can be useful to highlight field(s) with errors.
|
||||
///
|
||||
/// The form will rebuild to report the results.
|
||||
///
|
||||
/// See also:
|
||||
/// * [validate], which also validates descendant [FormField]s,
|
||||
/// and return true if there are no errors.
|
||||
Set<FormFieldState<Object?>> validateGranularly() {
|
||||
final Set<FormFieldState<Object?>> invalidFields = <FormFieldState<Object?>>{};
|
||||
_hasInteractedByUser = true;
|
||||
_forceRebuild();
|
||||
_validate(invalidFields);
|
||||
return invalidFields;
|
||||
}
|
||||
|
||||
bool _validate([Set<FormFieldState<Object?>>? invalidFields]) {
|
||||
bool hasError = false;
|
||||
String errorMessage = '';
|
||||
for (final FormFieldState<dynamic> field in _fields) {
|
||||
hasError = !field.validate() || hasError;
|
||||
final bool isFieldValid = field.validate();
|
||||
hasError = !isFieldValid || hasError;
|
||||
errorMessage += field.errorText ?? '';
|
||||
if (invalidFields != null && !isFieldValid) {
|
||||
invalidFields.add(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.isNotEmpty) {
|
||||
|
@ -272,6 +272,121 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'validateGranularly returns a set containing all, and only, invalid fields',
|
||||
(WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final UniqueKey validFieldsKey = UniqueKey();
|
||||
final UniqueKey invalidFieldsKey = UniqueKey();
|
||||
|
||||
const String validString = 'Valid string';
|
||||
const String invalidString = 'Invalid string';
|
||||
String? validator(String? s) => s == validString ? null : 'Error text';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
key: validFieldsKey,
|
||||
initialValue: validString,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
TextFormField(
|
||||
key: invalidFieldsKey,
|
||||
initialValue: invalidString,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
TextFormField(
|
||||
key: invalidFieldsKey,
|
||||
initialValue: invalidString,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
final Set<FormFieldState<dynamic>> validationResult = formKey.currentState!.validateGranularly();
|
||||
|
||||
expect(validationResult.length, equals(2));
|
||||
expect(validationResult.where((FormFieldState<dynamic> field) => field.widget.key == invalidFieldsKey).length, equals(2));
|
||||
expect(validationResult.where((FormFieldState<dynamic> field) => field.widget.key == validFieldsKey).length, equals(0));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Should announce error text when validateGranularly is called',
|
||||
(WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
const String validString = 'Valid string';
|
||||
String? validator(String? s) => s == validString ? null : 'error';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
initialValue: validString,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: '',
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
expect(find.text('error'), findsNothing);
|
||||
|
||||
formKey.currentState!.validateGranularly();
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('error'), findsOneWidget);
|
||||
|
||||
final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single;
|
||||
expect(announcement.message, 'error');
|
||||
expect(announcement.textDirection, TextDirection.ltr);
|
||||
expect(announcement.assertiveness, Assertiveness.assertive);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>();
|
||||
|
Loading…
x
Reference in New Issue
Block a user