Skip to content

Commit d0bb66a

Browse files
Merge pull request #1061 from CaLouro/remove_field_value_when_unregistering
Remove field values from internal maps when unregistered
2 parents adf5e71 + f02e64e commit d0bb66a

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

packages/flutter_form_builder/lib/src/form_builder.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ class FormBuilder extends StatefulWidget {
6363
/// Whether the form should auto focus on the first field that fails validation.
6464
final bool autoFocusOnValidationFailure;
6565

66+
/// Whether to clear the internal value of a field when it is unregistered.
67+
///
68+
/// Defaults to `false`.
69+
///
70+
/// When set to `true`, the form builder will not keep the internal values
71+
/// from disposed [FormBuilderField]s. This is useful for dynamic forms where
72+
/// fields are registered and unregistered due to state change.
73+
///
74+
/// This setting will have no effect when registering a field with the same
75+
/// name as the unregistered one.
76+
final bool clearValueOnUnregister;
77+
6678
/// Creates a container for form fields.
6779
///
6880
/// The [child] argument must not be null.
@@ -76,6 +88,7 @@ class FormBuilder extends StatefulWidget {
7688
this.skipDisabled = false,
7789
this.enabled = true,
7890
this.autoFocusOnValidationFailure = false,
91+
this.clearValueOnUnregister = false,
7992
}) : super(key: key);
8093

8194
static FormBuilderState? of(BuildContext context) =>
@@ -186,6 +199,10 @@ class FormBuilderState extends State<FormBuilder> {
186199
if (field == _fields[name]) {
187200
_fields.remove(name);
188201
_transformers.remove(name);
202+
if (widget.clearValueOnUnregister) {
203+
_instantValue.remove(name);
204+
_savedValue.remove(name);
205+
}
189206
} else {
190207
assert(() {
191208
// This is OK to ignore when you are intentionally replacing a field
@@ -195,8 +212,6 @@ class FormBuilderState extends State<FormBuilder> {
195212
return true;
196213
}());
197214
}
198-
// Removes internal field value
199-
// _savedValue.remove(name);
200215
}
201216

202217
void save() {

packages/flutter_form_builder/test/flutter_form_builder_test.dart

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,141 @@ void main() {
3333
expect(formSave(), isTrue);
3434
expect(formValue(textFieldName), isEmpty);
3535
});
36+
37+
testWidgets(
38+
'FormBuilder Dynamic Field -- keeping value',
39+
(WidgetTester tester) async {
40+
const String testWidgetName = 'test_widget_name';
41+
42+
await tester.pumpWidget(
43+
buildTestableFieldWidget(
44+
_DynamicFormFields(
45+
name: testWidgetName,
46+
valueTransformer: (value) {
47+
return value is String ? int.tryParse(value) : null;
48+
},
49+
),
50+
// the value is kept
51+
clearValueOnUnregister: false,
52+
),
53+
);
54+
55+
// Write an input and test the transformer
56+
formFieldDidChange(testWidgetName, '1');
57+
expect(int, formInstantValue(testWidgetName).runtimeType);
58+
expect(1, formInstantValue(testWidgetName));
59+
60+
// Remove the dynamic field from the widget tree
61+
final _DynamicFormFieldsState dynamicFieldState =
62+
tester.state(find.byType(_DynamicFormFields));
63+
dynamicFieldState.show = false;
64+
65+
// Pump the next frame, disposing the field's state
66+
await tester.pump();
67+
68+
// With the field unregistered, the form does not have its transformer
69+
// but it still has its value, now recovered as type String
70+
expect(String, formInstantValue(testWidgetName).runtimeType);
71+
expect('1', formInstantValue(testWidgetName));
72+
73+
// Show and recreate the field's state
74+
dynamicFieldState.show = true;
75+
await tester.pump();
76+
77+
// The transformer is registered again and with the internal value that
78+
// was kept, it's expected an int of value 1
79+
expect(int, formInstantValue(testWidgetName).runtimeType);
80+
expect(1, formInstantValue(testWidgetName));
81+
},
82+
);
83+
84+
testWidgets(
85+
'FormBuilder Dynamic Field -- clearing value',
86+
(WidgetTester tester) async {
87+
const String testWidgetName = 'test_widget_name';
88+
89+
await tester.pumpWidget(
90+
buildTestableFieldWidget(
91+
_DynamicFormFields(
92+
name: testWidgetName,
93+
valueTransformer: (value) {
94+
return value is String ? int.tryParse(value) : null;
95+
},
96+
),
97+
// the value is cleared
98+
clearValueOnUnregister: true,
99+
),
100+
);
101+
102+
// Write an input and test the transformer
103+
formFieldDidChange(testWidgetName, '1');
104+
await tester.pump();
105+
expect(int, formInstantValue(testWidgetName).runtimeType);
106+
expect(1, formInstantValue(testWidgetName));
107+
108+
// Remove the dynamic field from the widget tree
109+
final _DynamicFormFieldsState dynamicFieldState =
110+
tester.state(find.byType(_DynamicFormFields));
111+
dynamicFieldState.show = false;
112+
113+
// Pump the next frame, disposing the field's state
114+
await tester.pump();
115+
116+
// With the field unregistered, the form does not have its transformer,
117+
// and since the value was cleared, neither its value
118+
expect(Null, formInstantValue(testWidgetName).runtimeType);
119+
expect(null, formInstantValue(testWidgetName));
120+
121+
// Show and recreate the field's state
122+
dynamicFieldState.show = true;
123+
await tester.pump();
124+
125+
// A new input is needed to get another value
126+
formFieldDidChange(testWidgetName, '2');
127+
await tester.pump();
128+
expect(int, formInstantValue(testWidgetName).runtimeType);
129+
expect(2, formInstantValue(testWidgetName));
130+
},
131+
);
132+
}
133+
134+
// simple stateful widget that can hide and show its child with the intent of
135+
// disposing it from the tree
136+
class _DynamicFormFields extends StatefulWidget {
137+
const _DynamicFormFields({
138+
Key? key,
139+
required this.name,
140+
this.valueTransformer,
141+
}) : super(key: key);
142+
143+
final String name;
144+
final ValueTransformer? valueTransformer;
145+
146+
@override
147+
State<_DynamicFormFields> createState() => _DynamicFormFieldsState();
148+
}
149+
150+
class _DynamicFormFieldsState extends State<_DynamicFormFields> {
151+
bool _show = true;
152+
153+
bool get show => _show;
154+
155+
set show(bool value) => setState(() => _show = value);
156+
157+
@override
158+
Widget build(BuildContext context) {
159+
return Visibility(
160+
visible: _show,
161+
maintainState: false,
162+
child: FormBuilderField(
163+
name: widget.name,
164+
valueTransformer: widget.valueTransformer,
165+
builder: (FormFieldState<String?> field) {
166+
return TextField(
167+
onChanged: (value) => field.didChange(value),
168+
);
169+
},
170+
),
171+
);
172+
}
36173
}

packages/flutter_form_builder/test/form_builder_tester.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ final _formKey = GlobalKey<FormBuilderState>();
66
Widget buildTestableFieldWidget(
77
Widget widget, {
88
Map<String, dynamic> initialValue = const {},
9+
bool clearValueOnUnregister = false,
910
}) {
1011
return MaterialApp(
1112
home: Scaffold(
1213
body: FormBuilder(
1314
key: _formKey,
1415
initialValue: initialValue,
16+
clearValueOnUnregister: clearValueOnUnregister,
1517
child: widget,
1618
),
1719
),

0 commit comments

Comments
 (0)