Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new Form validation method #135578

Merged
merged 14 commits into from
Jan 9, 2024
Merged
31 changes: 29 additions & 2 deletions packages/flutter/lib/src/widgets/form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should have an empty new line before this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Renzo-Olivares , done. Anything else I should fix?

/// * [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) {
Expand Down
115 changes: 115 additions & 0 deletions packages/flutter/test/widgets/form_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>>();
Expand Down