Skip to content

Commit 9c8defe

Browse files
authored
Add fields getter to FormState (#180815)
This PR exposes a public `FormState.fields` getter that returns all registered `FormFieldState` instances associated with a `Form`. Fixes #110760 Fixes #67283 Partially fixes #107528 Partially fixes #108110 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 6d42ae5 commit 9c8defe

2 files changed

Lines changed: 155 additions & 0 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ class FormState extends State<Form> {
243243
bool _hasInteractedByUser = false;
244244
final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{};
245245

246+
/// The [FormFieldState] objects that are currently registered with this [Form].
247+
Iterable<FormFieldState<dynamic>> get fields => _fields;
248+
246249
// Called when a form field has changed. This will cause all form fields
247250
// to rebuild, useful if form fields have interdependencies.
248251
void _fieldDidChange() {

‎packages/flutter/test/widgets/form_test.dart‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,6 +1908,158 @@ void main() {
19081908
expect(find.text('foo'), findsOneWidget);
19091909
expect(find.text('bar'), findsOneWidget);
19101910
});
1911+
1912+
testWidgets('exposes all registered FormFieldStates with their values', (
1913+
WidgetTester tester,
1914+
) async {
1915+
final formKey = GlobalKey<FormState>();
1916+
1917+
await tester.pumpWidget(
1918+
MaterialApp(
1919+
home: Scaffold(
1920+
body: Form(
1921+
key: formKey,
1922+
child: Column(
1923+
children: <Widget>[
1924+
TextFormField(initialValue: 'A'),
1925+
TextFormField(initialValue: 'B'),
1926+
],
1927+
),
1928+
),
1929+
),
1930+
),
1931+
);
1932+
1933+
final FormState formState = formKey.currentState!;
1934+
1935+
expect(formState.fields.length, equals(2));
1936+
expect(formState.fields.map((field) => field.value), containsAll(<String>['A', 'B']));
1937+
});
1938+
1939+
testWidgets('reports all fields as invalid after validation errors', (WidgetTester tester) async {
1940+
final formKey = GlobalKey<FormState>();
1941+
String errorText(String? value) => '$value/error';
1942+
1943+
await tester.pumpWidget(
1944+
MaterialApp(
1945+
home: Scaffold(
1946+
body: Form(
1947+
key: formKey,
1948+
child: Column(
1949+
children: <Widget>[
1950+
TextFormField(initialValue: 'foo', validator: errorText),
1951+
TextFormField(initialValue: 'bar', validator: errorText),
1952+
],
1953+
),
1954+
),
1955+
),
1956+
),
1957+
);
1958+
1959+
formKey.currentState?.validate();
1960+
await tester.pump();
1961+
1962+
expect(find.text(errorText('foo')), findsOneWidget);
1963+
expect(find.text(errorText('bar')), findsOneWidget);
1964+
1965+
final List<FormFieldState<dynamic>> fields = formKey.currentState!.fields.toList();
1966+
1967+
// Expect all fields to be invalid.
1968+
expect(fields.every((field) => !field.isValid), isTrue);
1969+
});
1970+
1971+
testWidgets('isValid evaluates validity without updating error UI', (WidgetTester tester) async {
1972+
final formKey = GlobalKey<FormState>();
1973+
String errorText(String? value) => '$value/error';
1974+
1975+
await tester.pumpWidget(
1976+
MaterialApp(
1977+
home: Scaffold(
1978+
body: Form(
1979+
key: formKey,
1980+
child: TextFormField(initialValue: 'foo', validator: errorText),
1981+
),
1982+
),
1983+
),
1984+
);
1985+
1986+
final FormFieldState<dynamic> field = formKey.currentState!.fields.single;
1987+
1988+
expect(field.isValid, isFalse);
1989+
1990+
// No error UI should be shown.
1991+
expect(find.text(errorText('foo')), findsNothing);
1992+
expect(field.hasError, isFalse);
1993+
});
1994+
1995+
testWidgets('allows collecting and updating values from all field types', (
1996+
WidgetTester tester,
1997+
) async {
1998+
final formKey = GlobalKey<FormState>();
1999+
2000+
await tester.pumpWidget(
2001+
MaterialApp(
2002+
home: Scaffold(
2003+
body: Form(
2004+
key: formKey,
2005+
child: Column(
2006+
children: <Widget>[
2007+
TextFormField(key: const ValueKey('name'), initialValue: 'Name'),
2008+
TextFormField(key: const ValueKey('email'), initialValue: 'Email'),
2009+
FormField<int>(
2010+
key: const ValueKey('age'),
2011+
initialValue: 18,
2012+
builder: (field) => const SizedBox.shrink(),
2013+
),
2014+
DropdownButtonFormField<String>(
2015+
key: const ValueKey('animal'),
2016+
initialValue: 'cat',
2017+
items: const [
2018+
DropdownMenuItem(value: 'dog', child: SizedBox.shrink()),
2019+
DropdownMenuItem(value: 'cat', child: SizedBox.shrink()),
2020+
DropdownMenuItem(value: 'bird', child: SizedBox.shrink()),
2021+
],
2022+
onChanged: (_) {},
2023+
),
2024+
],
2025+
),
2026+
),
2027+
),
2028+
),
2029+
);
2030+
2031+
Map<String, dynamic> collectData() {
2032+
return {
2033+
for (final field in formKey.currentState!.fields)
2034+
if (field.widget.key case ValueKey<String>(:final value)) value: field.value,
2035+
};
2036+
}
2037+
2038+
expect(collectData(), <String, Object>{
2039+
'name': 'Name',
2040+
'email': 'Email',
2041+
'age': 18,
2042+
'animal': 'cat',
2043+
});
2044+
2045+
FormFieldState<T> field<T>(String key) => formKey.currentState!.fields
2046+
.whereType<FormFieldState<T>>()
2047+
.singleWhere((f) => f.widget.key == ValueKey(key));
2048+
2049+
field<String>('name').didChange('New Name');
2050+
field<String>('email').didChange('[email protected]');
2051+
field<int>('age').didChange(30);
2052+
field<String>('animal').didChange('dog');
2053+
2054+
await tester.pump();
2055+
2056+
expect(collectData(), <String, Object>{
2057+
'name': 'New Name',
2058+
'email': '[email protected]',
2059+
'age': 30,
2060+
'animal': 'dog',
2061+
});
2062+
});
19112063
}
19122064

19132065
class _PlatformAnnounceScenario {

0 commit comments

Comments
 (0)