Skip to content

Commit 8d17143

Browse files
Merge pull request #1272 from flutter-form-builder-ecosystem/fix-1252-dropdown-reset
fix: add deep compare to update dropdown items
2 parents d799db5 + 3487bfb commit 8d17143

File tree

4 files changed

+323
-46
lines changed

4 files changed

+323
-46
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
extension GenericValidator<T> on T? {
2+
bool emptyValidator() {
3+
if (this == null) return true;
4+
if (this is Iterable) return (this as Iterable).isEmpty;
5+
if (this is String) return (this as String).isEmpty;
6+
if (this is List) return (this as List).isEmpty;
7+
if (this is Map) return (this as Map).isEmpty;
8+
if (this is Set) return (this as Set).isEmpty;
9+
return false;
10+
}
11+
}

lib/src/fields/form_builder_dropdown.dart

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:flutter/material.dart';
23
import 'package:flutter_form_builder/flutter_form_builder.dart';
4+
import 'package:flutter_form_builder/src/extensions/generic_validator.dart';
35

46
/// Field for Dropdown button
57
class FormBuilderDropdown<T> extends FormBuilderFieldDecoration<T> {
@@ -299,7 +301,28 @@ class _FormBuilderDropdownState<T>
299301
@override
300302
void didUpdateWidget(covariant FormBuilderDropdown<T> oldWidget) {
301303
super.didUpdateWidget(oldWidget);
302-
if (widget.items != oldWidget.items) {
304+
305+
final oldValues = oldWidget.items.map((e) => e.value).toList();
306+
final currentlyValues = widget.items.map((e) => e.value).toList();
307+
final oldChilds = oldWidget.items.map((e) => e.child.toString()).toList();
308+
final currentlyChilds =
309+
widget.items.map((e) => e.child.toString()).toList();
310+
311+
if (!currentlyValues.contains(initialValue) &&
312+
!initialValue.emptyValidator()) {
313+
assert(
314+
currentlyValues.contains(initialValue) && initialValue.emptyValidator(),
315+
'The initialValue [$initialValue] is not in the list of items or is not null or empty. '
316+
'Please provide one of the items as the initialValue or update your initial value. '
317+
'By default, will apply [null] to field value',
318+
);
319+
setValue(null);
320+
}
321+
322+
if ((!listEquals(oldChilds, currentlyChilds) ||
323+
!listEquals(oldValues, currentlyValues)) &&
324+
(currentlyValues.contains(initialValue) ||
325+
initialValue.emptyValidator())) {
303326
setValue(initialValue);
304327
}
305328
}

test/form_builder_dropdown_test.dart

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_form_builder/src/fields/form_builder_dropdown.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
import '../../form_builder_tester.dart';
6+
7+
void main() {
8+
group('FormBuilderDropdown --', () {
9+
testWidgets('basic', (WidgetTester tester) async {
10+
const widgetName = 'd1';
11+
final testWidget = FormBuilderDropdown<int>(
12+
name: widgetName,
13+
items: const [
14+
DropdownMenuItem(
15+
value: 1,
16+
child: Text('One'),
17+
),
18+
DropdownMenuItem(
19+
value: 2,
20+
child: Text('Two'),
21+
),
22+
DropdownMenuItem(
23+
value: 3,
24+
child: Text('Three'),
25+
),
26+
],
27+
);
28+
final widgetFinder = find.byWidget(testWidget);
29+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
30+
31+
expect(formSave(), isTrue);
32+
expect(formValue(widgetName), isNull);
33+
await tester.tap(widgetFinder);
34+
await tester.pumpAndSettle();
35+
await tester.tap(find.text('Three').last);
36+
await tester.pumpAndSettle();
37+
expect(formSave(), isTrue);
38+
expect(formValue(widgetName), equals(3));
39+
40+
await tester.tap(find.text('Three').last);
41+
await tester.pumpAndSettle();
42+
await tester.tap(find.text('One').last);
43+
await tester.pumpAndSettle();
44+
expect(formSave(), isTrue);
45+
expect(formValue(widgetName), equals(1));
46+
});
47+
testWidgets('reset to initial value when update items',
48+
(WidgetTester tester) async {
49+
const widgetName = 'dropdown_field';
50+
const buttonKey = Key('update_button');
51+
52+
// Define the initial and updated items for the dropdown
53+
const List<DropdownMenuItem<int>> initialItems = [
54+
DropdownMenuItem(value: 1, child: Text('Option 1')),
55+
DropdownMenuItem(value: 2, child: Text('Option 2')),
56+
];
57+
58+
const List<DropdownMenuItem<int>> updatedItems = [
59+
DropdownMenuItem(value: 3, child: Text('Option 3')),
60+
DropdownMenuItem(value: 4, child: Text('Option 4')),
61+
];
62+
63+
// Build the test widget tree with the initial items
64+
const testWidget = MyTestWidget(
65+
initialItems: initialItems,
66+
updatedItems: updatedItems,
67+
initialValue: 1,
68+
updatedInitialValue: 3,
69+
fieldName: widgetName,
70+
buttonKey: buttonKey,
71+
);
72+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
73+
74+
// Verify the initial value of field
75+
expect(formSave(), isTrue);
76+
expect(formValue(widgetName), equals(1));
77+
78+
// Tap button and update the dropdown items
79+
final buttonFinder = find.byKey(buttonKey);
80+
await tester.tap(buttonFinder);
81+
await tester.pumpAndSettle();
82+
83+
// Verify the updated value of field
84+
expect(formSave(), isTrue);
85+
expect(formValue(widgetName), equals(3));
86+
});
87+
testWidgets('reset to initial value when update items with same values',
88+
(WidgetTester tester) async {
89+
const widgetName = 'dropdown_field';
90+
const buttonKey = Key('update_button');
91+
92+
// Define the initial and updated items for the dropdown
93+
const List<DropdownMenuItem<int>> initialItems = [
94+
DropdownMenuItem(value: 1, child: Text('Option 1')),
95+
DropdownMenuItem(value: 2, child: Text('Option 2')),
96+
];
97+
98+
const List<DropdownMenuItem<int>> updatedItems = [
99+
DropdownMenuItem(value: 1, child: Text('Option 3')),
100+
DropdownMenuItem(value: 2, child: Text('Option 4')),
101+
];
102+
103+
// Build the test widget tree with the initial items
104+
const testWidget = MyTestWidget(
105+
initialItems: initialItems,
106+
updatedItems: updatedItems,
107+
initialValue: 1,
108+
fieldName: widgetName,
109+
buttonKey: buttonKey,
110+
);
111+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
112+
113+
// Verify the initial value of field
114+
expect(formSave(), isTrue);
115+
expect(formValue(widgetName), equals(1));
116+
117+
// Update dropdown selected value
118+
await tester.tap(find.byType(FormBuilderDropdown<int>));
119+
await tester.pumpAndSettle();
120+
await tester.tap(find.text('Option 2'));
121+
await tester.pumpAndSettle();
122+
expect(formSave(), isTrue);
123+
expect(formValue(widgetName), equals(2));
124+
125+
// Tap button and update the dropdown items
126+
final buttonFinder = find.byKey(buttonKey);
127+
await tester.tap(buttonFinder);
128+
await tester.pumpAndSettle();
129+
130+
// Verify the updated value of field
131+
expect(formSave(), isTrue);
132+
expect(formValue(widgetName), equals(1));
133+
});
134+
135+
testWidgets('reset to initial value when update items with same children',
136+
(WidgetTester tester) async {
137+
const widgetName = 'dropdown_field';
138+
const buttonKey = Key('update_button');
139+
const option1 = Text('Option 1');
140+
const option2 = Text('Option 2');
141+
142+
// Define the initial and updated items for the dropdown
143+
const List<DropdownMenuItem<int>> initialItems = [
144+
DropdownMenuItem(value: 1, child: option1),
145+
DropdownMenuItem(value: 2, child: option2),
146+
];
147+
148+
const List<DropdownMenuItem<int>> updatedItems = [
149+
DropdownMenuItem(value: 3, child: option1),
150+
DropdownMenuItem(value: 4, child: option2),
151+
];
152+
153+
// Build the test widget tree with the initial items
154+
const testWidget = MyTestWidget(
155+
initialItems: initialItems,
156+
updatedItems: updatedItems,
157+
initialValue: 1,
158+
updatedInitialValue: 3,
159+
fieldName: widgetName,
160+
buttonKey: buttonKey,
161+
);
162+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
163+
164+
// Verify the initial value of field
165+
expect(formSave(), isTrue);
166+
expect(formValue(widgetName), equals(1));
167+
168+
// Update dropdown selected value
169+
await tester.tap(find.byType(FormBuilderDropdown<int>));
170+
await tester.pumpAndSettle();
171+
await tester.tap(find.text('Option 2'));
172+
await tester.pumpAndSettle();
173+
expect(formSave(), isTrue);
174+
expect(formValue(widgetName), equals(2));
175+
176+
// Tap button and update the dropdown items
177+
final buttonFinder = find.byKey(buttonKey);
178+
await tester.tap(buttonFinder);
179+
await tester.pumpAndSettle();
180+
181+
// Verify the updated value of field
182+
expect(formSave(), isTrue);
183+
expect(formValue(widgetName), equals(3));
184+
});
185+
testWidgets('maintain initial value when update to equals items',
186+
(WidgetTester tester) async {
187+
const widgetName = 'dropdown_field';
188+
const buttonKey = Key('update_button');
189+
190+
// Define the initial and updated items for the dropdown
191+
const List<DropdownMenuItem<int>> initialItems = [
192+
DropdownMenuItem(value: 1, child: Text('Option 1')),
193+
DropdownMenuItem(value: 2, child: Text('Option 2')),
194+
];
195+
196+
// Build the test widget tree with the initial items
197+
const testWidget = MyTestWidget(
198+
initialItems: initialItems,
199+
updatedItems: initialItems,
200+
initialValue: 1,
201+
fieldName: widgetName,
202+
buttonKey: buttonKey,
203+
);
204+
await tester.pumpWidget(buildTestableFieldWidget(testWidget));
205+
206+
// Verify the initial value of field
207+
expect(formSave(), isTrue);
208+
expect(formValue(widgetName), equals(1));
209+
210+
// Update dropdown selected value
211+
await tester.tap(find.byType(FormBuilderDropdown<int>));
212+
await tester.pumpAndSettle();
213+
await tester.tap(find.text('Option 2'));
214+
await tester.pumpAndSettle();
215+
expect(formSave(), isTrue);
216+
expect(formValue(widgetName), equals(2));
217+
218+
// Tap button and update the dropdown items
219+
final buttonFinder = find.byKey(buttonKey);
220+
await tester.tap(buttonFinder);
221+
await tester.pumpAndSettle();
222+
223+
// Verify the updated value of field
224+
expect(formSave(), isTrue);
225+
expect(formValue(widgetName), equals(2));
226+
});
227+
});
228+
}
229+
230+
class MyTestWidget<T> extends StatefulWidget {
231+
final List<DropdownMenuItem<T>> initialItems;
232+
final List<DropdownMenuItem<T>> updatedItems;
233+
final T? initialValue;
234+
final T? updatedInitialValue;
235+
final String fieldName;
236+
final Key? buttonKey;
237+
238+
const MyTestWidget({
239+
super.key,
240+
required this.initialItems,
241+
this.initialValue,
242+
this.updatedItems = const [],
243+
required this.fieldName,
244+
required this.buttonKey,
245+
this.updatedInitialValue,
246+
});
247+
248+
@override
249+
State<MyTestWidget> createState() => _MyTestWidgetState<T>();
250+
}
251+
252+
class _MyTestWidgetState<T> extends State<MyTestWidget> {
253+
T? _initialValue;
254+
List<DropdownMenuItem<T>> _items = [];
255+
256+
@override
257+
void initState() {
258+
super.initState();
259+
_items = widget.initialItems as List<DropdownMenuItem<T>>;
260+
_initialValue = widget.initialValue;
261+
}
262+
263+
@override
264+
Widget build(BuildContext context) {
265+
return Column(
266+
children: [
267+
FormBuilderDropdown<T>(
268+
name: 'dropdown_field',
269+
items: _items,
270+
initialValue: _initialValue,
271+
onChanged: (value) {},
272+
),
273+
ElevatedButton(
274+
key: widget.buttonKey,
275+
onPressed: () {
276+
setState(() {
277+
_items = widget.updatedItems as List<DropdownMenuItem<T>>;
278+
if (widget.updatedInitialValue != null) {
279+
_initialValue = widget.updatedInitialValue;
280+
}
281+
});
282+
},
283+
child: const Text('update'),
284+
)
285+
],
286+
);
287+
}
288+
}

0 commit comments

Comments
 (0)