Skip to content

Commit 07cc3f7

Browse files
authored
Form fields onChange callback should be called on reset (#134295)
## Description This PR fixes form fields in order to call the `onChange` callback when the form is reset. This change is based on the work done in flutter/flutter#123108. I considered adding the `onChange` callback to the `FormField` superclass but it would break existing code because two of the three subclasses defines the `onChange` callback with `ValueChanged<String>?` type and the third one defines it with `ValueChanged<String?>?`. ## Related Issue Fixes flutter/flutter#123009. ## Tests Adds 3 tests.
1 parent 518b775 commit 07cc3f7

File tree

6 files changed

+177
-19
lines changed

6 files changed

+177
-19
lines changed

packages/flutter/lib/src/cupertino/text_form_field_row.dart

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class CupertinoTextFormFieldRow extends FormField<String> {
133133
int? minLines,
134134
bool expands = false,
135135
int? maxLength,
136-
ValueChanged<String>? onChanged,
136+
this.onChanged,
137137
GestureTapCallback? onTap,
138138
VoidCallback? onEditingComplete,
139139
ValueChanged<String>? onFieldSubmitted,
@@ -179,9 +179,7 @@ class CupertinoTextFormFieldRow extends FormField<String> {
179179

180180
void onChangedHandler(String value) {
181181
field.didChange(value);
182-
if (onChanged != null) {
183-
onChanged(value);
184-
}
182+
onChanged?.call(value);
185183
}
186184

187185
return CupertinoFormRow(
@@ -260,6 +258,9 @@ class CupertinoTextFormFieldRow extends FormField<String> {
260258
/// initialize its [TextEditingController.text] with [initialValue].
261259
final TextEditingController? controller;
262260

261+
/// {@macro flutter.material.TextFormField.onChanged}
262+
final ValueChanged<String>? onChanged;
263+
263264
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
264265
return CupertinoAdaptiveTextSelectionToolbar.editableText(
265266
editableTextState: editableTextState,
@@ -328,13 +329,11 @@ class _CupertinoTextFormFieldRowState extends FormFieldState<String> {
328329

329330
@override
330331
void reset() {
332+
// Set the controller value before calling super.reset() to let
333+
// _handleControllerChanged suppress the change.
334+
_effectiveController!.text = widget.initialValue!;
331335
super.reset();
332-
333-
if (widget.initialValue != null) {
334-
setState(() {
335-
_effectiveController!.text = widget.initialValue!;
336-
});
337-
}
336+
_cupertinoTextFormFieldRow.onChanged?.call(_effectiveController!.text);
338337
}
339338

340339
void _handleControllerChanged() {

packages/flutter/lib/src/material/dropdown.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,13 +1736,12 @@ class DropdownButtonFormField<T> extends FormField<T> {
17361736
}
17371737

17381738
class _DropdownButtonFormFieldState<T> extends FormFieldState<T> {
1739+
DropdownButtonFormField<T> get _dropdownButtonFormField => widget as DropdownButtonFormField<T>;
17391740

17401741
@override
17411742
void didChange(T? value) {
17421743
super.didChange(value);
1743-
final DropdownButtonFormField<T> dropdownButtonFormField = widget as DropdownButtonFormField<T>;
1744-
assert(dropdownButtonFormField.onChanged != null);
1745-
dropdownButtonFormField.onChanged!(value);
1744+
_dropdownButtonFormField.onChanged!(value);
17461745
}
17471746

17481747
@override
@@ -1752,4 +1751,10 @@ class _DropdownButtonFormFieldState<T> extends FormFieldState<T> {
17521751
setValue(widget.initialValue);
17531752
}
17541753
}
1754+
1755+
@override
1756+
void reset() {
1757+
super.reset();
1758+
_dropdownButtonFormField.onChanged!(value);
1759+
}
17551760
}

packages/flutter/lib/src/material/text_form_field.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class TextFormField extends FormField<String> {
131131
int? minLines,
132132
bool expands = false,
133133
int? maxLength,
134-
ValueChanged<String>? onChanged,
134+
this.onChanged,
135135
GestureTapCallback? onTap,
136136
TapRegionCallback? onTapOutside,
137137
VoidCallback? onEditingComplete,
@@ -193,9 +193,7 @@ class TextFormField extends FormField<String> {
193193
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
194194
void onChangedHandler(String value) {
195195
field.didChange(value);
196-
if (onChanged != null) {
197-
onChanged(value);
198-
}
196+
onChanged?.call(value);
199197
}
200198
return UnmanagedRestorationScope(
201199
bucket: field.bucket,
@@ -272,6 +270,12 @@ class TextFormField extends FormField<String> {
272270
/// initialize its [TextEditingController.text] with [initialValue].
273271
final TextEditingController? controller;
274272

273+
/// {@template flutter.material.TextFormField.onChanged}
274+
/// Called when the user initiates a change to the TextField's
275+
/// value: when they have inserted or deleted text or reset the form.
276+
/// {@endtemplate}
277+
final ValueChanged<String>? onChanged;
278+
275279
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
276280
return AdaptiveTextSelectionToolbar.editableText(
277281
editableTextState: editableTextState,
@@ -365,10 +369,11 @@ class _TextFormFieldState extends FormFieldState<String> {
365369

366370
@override
367371
void reset() {
368-
// setState will be called in the superclass, so even though state is being
369-
// manipulated, no setState call is needed here.
372+
// Set the controller value before calling super.reset() to let
373+
// _handleControllerChanged suppress the change.
370374
_effectiveController.text = widget.initialValue ?? '';
371375
super.reset();
376+
_textFormField.onChanged?.call(_effectiveController.text);
372377
}
373378

374379
void _handleControllerChanged() {

packages/flutter/test/cupertino/text_form_field_row_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,4 +490,43 @@ void main() {
490490
final CupertinoTextField rtlTextFieldWidget = tester.widget(rtlTextFieldFinder);
491491
expect(rtlTextFieldWidget.textDirection, TextDirection.rtl);
492492
});
493+
494+
testWidgets('CupertinoTextFormFieldRow onChanged is called when the form is reset', (WidgetTester tester) async {
495+
// Regression test for https://github.com/flutter/flutter/issues/123009.
496+
final GlobalKey<FormFieldState<String>> stateKey = GlobalKey<FormFieldState<String>>();
497+
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
498+
String value = 'initialValue';
499+
500+
await tester.pumpWidget(
501+
CupertinoApp(
502+
home: Center(
503+
child: Form(
504+
key: formKey,
505+
child: CupertinoTextFormFieldRow(
506+
key: stateKey,
507+
initialValue: value,
508+
onChanged: (String newValue) {
509+
value = newValue;
510+
},
511+
),
512+
),
513+
),
514+
),
515+
);
516+
517+
// Initial value is 'initialValue'.
518+
expect(stateKey.currentState!.value, 'initialValue');
519+
expect(value, 'initialValue');
520+
521+
// Change value to 'changedValue'.
522+
await tester.enterText(find.byType(CupertinoTextField), 'changedValue');
523+
expect(stateKey.currentState!.value,'changedValue');
524+
expect(value, 'changedValue');
525+
526+
// Should be back to 'initialValue' when the form is reset.
527+
formKey.currentState!.reset();
528+
await tester.pump();
529+
expect(stateKey.currentState!.value,'initialValue');
530+
expect(value, 'initialValue');
531+
});
493532
}

packages/flutter/test/material/dropdown_form_field_test.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,4 +1231,52 @@ void main() {
12311231
inkWell = tester.widget<InkWell>(find.byType(InkWell));
12321232
expect(inkWell.borderRadius, errorBorderRadius);
12331233
});
1234+
1235+
testWidgets('DropdownButtonFormField onChanged is called when the form is reset', (WidgetTester tester) async {
1236+
// Regression test for https://github.com/flutter/flutter/issues/123009.
1237+
final GlobalKey<FormFieldState<String>> stateKey = GlobalKey<FormFieldState<String>>();
1238+
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
1239+
String? value;
1240+
1241+
await tester.pumpWidget(
1242+
MaterialApp(
1243+
home: Material(
1244+
child: Form(
1245+
key: formKey,
1246+
child: DropdownButtonFormField<String>(
1247+
key: stateKey,
1248+
value: 'One',
1249+
items: <String>['One', 'Two', 'Free', 'Four']
1250+
.map<DropdownMenuItem<String>>((String value) {
1251+
return DropdownMenuItem<String>(
1252+
value: value,
1253+
child: Text(value),
1254+
);
1255+
}).toList(),
1256+
onChanged: (String? newValue) {
1257+
value = newValue;
1258+
},
1259+
),
1260+
),
1261+
),
1262+
),
1263+
);
1264+
1265+
// Initial value is 'One'.
1266+
expect(value, isNull);
1267+
expect(stateKey.currentState!.value, equals('One'));
1268+
1269+
// Select 'Two'.
1270+
await tester.tap(find.text('One'));
1271+
await tester.pumpAndSettle();
1272+
await tester.tap(find.text('Two').last);
1273+
await tester.pumpAndSettle();
1274+
expect(value, equals('Two'));
1275+
expect(stateKey.currentState!.value, equals('Two'));
1276+
1277+
// Should be back to 'One' when the form is reset.
1278+
formKey.currentState!.reset();
1279+
expect(value, equals('One'));
1280+
expect(stateKey.currentState!.value, equals('One'));
1281+
});
12341282
}

packages/flutter/test/material/text_form_field_test.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,31 @@ void main() {
816816
expect(find.text('initialValue'), findsOneWidget);
817817
});
818818

819+
testWidgetsWithLeakTracking('reset resets the text fields value to the controller initial value', (WidgetTester tester) async {
820+
final TextEditingController controller = TextEditingController(text: 'initialValue');
821+
addTearDown(controller.dispose);
822+
823+
await tester.pumpWidget(
824+
MaterialApp(
825+
home: Material(
826+
child: Center(
827+
child: TextFormField(
828+
controller: controller,
829+
),
830+
),
831+
),
832+
),
833+
);
834+
835+
await tester.enterText(find.byType(TextFormField), 'changedValue');
836+
837+
final FormFieldState<String> state = tester.state<FormFieldState<String>>(find.byType(TextFormField));
838+
state.reset();
839+
840+
expect(find.text('changedValue'), findsNothing);
841+
expect(find.text('initialValue'), findsOneWidget);
842+
});
843+
819844
// Regression test for https://github.com/flutter/flutter/issues/34847.
820845
testWidgetsWithLeakTracking("didChange resets the text field's value to empty when passed null", (WidgetTester tester) async {
821846
await tester.pumpWidget(
@@ -1478,4 +1503,41 @@ void main() {
14781503
await tester.pump();
14791504
expect(textField.cursorColor, errorColor);
14801505
});
1506+
1507+
testWidgetsWithLeakTracking('TextFormField onChanged is called when the form is reset', (WidgetTester tester) async {
1508+
// Regression test for https://github.com/flutter/flutter/issues/123009.
1509+
final GlobalKey<FormFieldState<String>> stateKey = GlobalKey<FormFieldState<String>>();
1510+
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
1511+
String value = 'initialValue';
1512+
1513+
await tester.pumpWidget(MaterialApp(
1514+
home: Scaffold(
1515+
body: Form(
1516+
key: formKey,
1517+
child: TextFormField(
1518+
key: stateKey,
1519+
initialValue: value,
1520+
onChanged: (String newValue) {
1521+
value = newValue;
1522+
},
1523+
),
1524+
),
1525+
),
1526+
));
1527+
1528+
// Initial value is 'initialValue'.
1529+
expect(stateKey.currentState!.value, 'initialValue');
1530+
expect(value, 'initialValue');
1531+
1532+
// Change value to 'changedValue'.
1533+
await tester.enterText(find.byType(TextField), 'changedValue');
1534+
expect(stateKey.currentState!.value,'changedValue');
1535+
expect(value, 'changedValue');
1536+
1537+
// Should be back to 'initialValue' when the form is reset.
1538+
formKey.currentState!.reset();
1539+
await tester.pump();
1540+
expect(stateKey.currentState!.value,'initialValue');
1541+
expect(value, 'initialValue');
1542+
});
14811543
}

0 commit comments

Comments
 (0)