Skip to content

Commit 0c40f21

Browse files
authored
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
1 parent 536de5e commit 0c40f21

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

packages/flutter/lib/src/widgets/form.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,18 +291,45 @@ class FormState extends State<Form> {
291291
/// returns true if there are no errors.
292292
///
293293
/// The form will rebuild to report the results.
294+
///
295+
/// See also:
296+
/// * [validateGranularly], which also validates descendant [FormField]s,
297+
/// but instead returns a [Set] of fields with errors.
294298
bool validate() {
295299
_hasInteractedByUser = true;
296300
_forceRebuild();
297301
return _validate();
298302
}
299303

300-
bool _validate() {
304+
305+
/// Validates every [FormField] that is a descendant of this [Form], and
306+
/// returns a [Set] of [FormFieldState] of the invalid field(s) only, if any.
307+
///
308+
/// This method can be useful to highlight field(s) with errors.
309+
///
310+
/// The form will rebuild to report the results.
311+
///
312+
/// See also:
313+
/// * [validate], which also validates descendant [FormField]s,
314+
/// and return true if there are no errors.
315+
Set<FormFieldState<Object?>> validateGranularly() {
316+
final Set<FormFieldState<Object?>> invalidFields = <FormFieldState<Object?>>{};
317+
_hasInteractedByUser = true;
318+
_forceRebuild();
319+
_validate(invalidFields);
320+
return invalidFields;
321+
}
322+
323+
bool _validate([Set<FormFieldState<Object?>>? invalidFields]) {
301324
bool hasError = false;
302325
String errorMessage = '';
303326
for (final FormFieldState<dynamic> field in _fields) {
304-
hasError = !field.validate() || hasError;
327+
final bool isFieldValid = field.validate();
328+
hasError = !isFieldValid || hasError;
305329
errorMessage += field.errorText ?? '';
330+
if (invalidFields != null && !isFieldValid) {
331+
invalidFields.add(field);
332+
}
306333
}
307334

308335
if (errorMessage.isNotEmpty) {

packages/flutter/test/widgets/form_test.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,121 @@ void main() {
272272
},
273273
);
274274

275+
testWidgets(
276+
'validateGranularly returns a set containing all, and only, invalid fields',
277+
(WidgetTester tester) async {
278+
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
279+
final UniqueKey validFieldsKey = UniqueKey();
280+
final UniqueKey invalidFieldsKey = UniqueKey();
281+
282+
const String validString = 'Valid string';
283+
const String invalidString = 'Invalid string';
284+
String? validator(String? s) => s == validString ? null : 'Error text';
285+
286+
Widget builder() {
287+
return MaterialApp(
288+
home: MediaQuery(
289+
data: const MediaQueryData(),
290+
child: Directionality(
291+
textDirection: TextDirection.ltr,
292+
child: Center(
293+
child: Material(
294+
child: Form(
295+
key: formKey,
296+
child: ListView(
297+
children: <Widget>[
298+
TextFormField(
299+
key: validFieldsKey,
300+
initialValue: validString,
301+
validator: validator,
302+
autovalidateMode: AutovalidateMode.disabled,
303+
),
304+
TextFormField(
305+
key: invalidFieldsKey,
306+
initialValue: invalidString,
307+
validator: validator,
308+
autovalidateMode: AutovalidateMode.disabled,
309+
),
310+
TextFormField(
311+
key: invalidFieldsKey,
312+
initialValue: invalidString,
313+
validator: validator,
314+
autovalidateMode: AutovalidateMode.disabled,
315+
),
316+
],
317+
),
318+
),
319+
),
320+
),
321+
),
322+
),
323+
);
324+
}
325+
326+
await tester.pumpWidget(builder());
327+
328+
final Set<FormFieldState<dynamic>> validationResult = formKey.currentState!.validateGranularly();
329+
330+
expect(validationResult.length, equals(2));
331+
expect(validationResult.where((FormFieldState<dynamic> field) => field.widget.key == invalidFieldsKey).length, equals(2));
332+
expect(validationResult.where((FormFieldState<dynamic> field) => field.widget.key == validFieldsKey).length, equals(0));
333+
},
334+
);
335+
336+
testWidgets(
337+
'Should announce error text when validateGranularly is called',
338+
(WidgetTester tester) async {
339+
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
340+
const String validString = 'Valid string';
341+
String? validator(String? s) => s == validString ? null : 'error';
342+
343+
Widget builder() {
344+
return MaterialApp(
345+
home: MediaQuery(
346+
data: const MediaQueryData(),
347+
child: Directionality(
348+
textDirection: TextDirection.ltr,
349+
child: Center(
350+
child: Material(
351+
child: Form(
352+
key: formKey,
353+
child: ListView(
354+
children: <Widget>[
355+
TextFormField(
356+
initialValue: validString,
357+
validator: validator,
358+
autovalidateMode: AutovalidateMode.disabled,
359+
),
360+
TextFormField(
361+
initialValue: '',
362+
validator: validator,
363+
autovalidateMode: AutovalidateMode.disabled,
364+
),
365+
],
366+
),
367+
),
368+
),
369+
),
370+
),
371+
),
372+
);
373+
}
374+
375+
await tester.pumpWidget(builder());
376+
expect(find.text('error'), findsNothing);
377+
378+
formKey.currentState!.validateGranularly();
379+
380+
await tester.pump();
381+
expect(find.text('error'), findsOneWidget);
382+
383+
final CapturedAccessibilityAnnouncement announcement = tester.takeAnnouncements().single;
384+
expect(announcement.message, 'error');
385+
expect(announcement.textDirection, TextDirection.ltr);
386+
expect(announcement.assertiveness, Assertiveness.assertive);
387+
},
388+
);
389+
275390
testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
276391
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
277392
final GlobalKey<FormFieldState<String>> fieldKey = GlobalKey<FormFieldState<String>>();

0 commit comments

Comments
 (0)