1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'text_theme.dart';
6library;
7
8import 'package:flutter/services.dart';
9import 'package:flutter/widgets.dart';
10
11import 'dropdown_menu.dart';
12import 'menu_style.dart';
13
14/// A [FormField] that contains a [DropdownMenu].
15///
16/// This is a convenience widget that wraps a [DropdownMenu] widget in a
17/// [FormField].
18///
19/// A [Form] ancestor is not required. The [Form] allows one to
20/// save, reset, or validate multiple fields at once. To use without a [Form],
21/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
22/// save or reset the form field.
23///
24/// The `value` parameter maps to [FormField.initialValue].
25///
26/// See also:
27///
28/// * [DropdownMenu], which is the underlying text field without the [Form]
29/// integration.
30class DropdownMenuFormField<T> extends FormField<T> {
31 /// Creates a [DropdownMenu] widget that is a [FormField].
32 ///
33 /// For a description of the `onSaved`, `validator`, or `autovalidateMode`
34 /// parameters, see [FormField]. For the rest, see [DropdownMenu].
35 DropdownMenuFormField({
36 super.key,
37 bool enabled = true,
38 double? width,
39 double? menuHeight,
40 Widget? leadingIcon,
41 Widget? trailingIcon,
42 Widget? label,
43 String? hintText,
44 String? helperText,
45 Widget? selectedTrailingIcon,
46 bool enableFilter = false,
47 bool enableSearch = true,
48 TextInputType? keyboardType,
49 TextStyle? textStyle,
50 TextAlign textAlign = TextAlign.start,
51 // TODO(bleroux): Clean this up once `InputDecorationTheme` is fully normalized.
52 Object? inputDecorationTheme,
53 MenuStyle? menuStyle,
54 this.controller,
55 T? initialSelection,
56 this.onSelected,
57 FocusNode? focusNode,
58 bool? requestFocusOnTap,
59 EdgeInsetsGeometry? expandedInsets,
60 Offset? alignmentOffset,
61 FilterCallback<T>? filterCallback,
62 SearchCallback<T>? searchCallback,
63 required this.dropdownMenuEntries,
64 List<TextInputFormatter>? inputFormatters,
65 DropdownMenuCloseBehavior closeBehavior = DropdownMenuCloseBehavior.all,
66 int maxLines = 1,
67 TextInputAction? textInputAction,
68 super.restorationId,
69 super.onSaved,
70 AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
71 super.validator,
72 super.forceErrorText,
73 }) : super(
74 initialValue: initialSelection,
75 autovalidateMode: autovalidateMode,
76 builder: (FormFieldState<T> field) {
77 final _DropdownMenuFormFieldState<T> state = field as _DropdownMenuFormFieldState<T>;
78 void onSelectedHandler(T? value) {
79 field.didChange(value);
80 onSelected?.call(value);
81 }
82
83 return UnmanagedRestorationScope(
84 bucket: field.bucket,
85 child: DropdownMenu<T>(
86 restorationId: restorationId,
87 enabled: enabled,
88 width: width,
89 menuHeight: menuHeight,
90 leadingIcon: leadingIcon,
91 trailingIcon: trailingIcon,
92 label: label,
93 hintText: hintText,
94 helperText: helperText,
95 errorText: state.errorText,
96 selectedTrailingIcon: selectedTrailingIcon,
97 enableFilter: enableFilter,
98 enableSearch: enableSearch,
99 keyboardType: keyboardType,
100 textStyle: textStyle,
101 textAlign: textAlign,
102 inputDecorationTheme: inputDecorationTheme,
103 menuStyle: menuStyle,
104 controller: controller,
105 initialSelection: state.value,
106 onSelected: onSelectedHandler,
107 focusNode: focusNode,
108 requestFocusOnTap: requestFocusOnTap,
109 expandedInsets: expandedInsets,
110 alignmentOffset: alignmentOffset,
111 filterCallback: filterCallback,
112 searchCallback: searchCallback,
113 inputFormatters: inputFormatters,
114 closeBehavior: closeBehavior,
115 dropdownMenuEntries: dropdownMenuEntries,
116 maxLines: maxLines,
117 textInputAction: textInputAction,
118 ),
119 );
120 },
121 );
122
123 /// The callback is called when a selection is made.
124 ///
125 /// Defaults to null. If null, only the text field is updated.
126 final ValueChanged<T?>? onSelected;
127
128 /// Controls the text being edited.
129 ///
130 /// If null, this widget will create its own [TextEditingController].
131 final TextEditingController? controller;
132
133 /// Descriptions of the menu items in the [DropdownMenuFormField].
134 ///
135 /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]
136 /// is provided. If this is an empty list, the menu will be empty and only
137 /// contain space for padding.
138 final List<DropdownMenuEntry<T>> dropdownMenuEntries;
139
140 @override
141 FormFieldState<T> createState() => _DropdownMenuFormFieldState<T>();
142}
143
144class _DropdownMenuFormFieldState<T> extends FormFieldState<T> {
145 DropdownMenuFormField<T> get _dropdownMenuFormField => widget as DropdownMenuFormField<T>;
146
147 RestorableTextEditingController? _restorableController;
148
149 @override
150 void initState() {
151 super.initState();
152 _createRestorableController(widget.initialValue);
153 }
154
155 void _createRestorableController(T? initialValue) {
156 assert(_restorableController == null);
157 _restorableController = RestorableTextEditingController.fromValue(
158 TextEditingValue(text: _findLabelByValue(initialValue)),
159 );
160 if (!restorePending) {
161 _registerRestorableController();
162 }
163 }
164
165 @override
166 void didUpdateWidget(DropdownMenuFormField<T> oldWidget) {
167 super.didUpdateWidget(oldWidget);
168 if (oldWidget.initialValue != widget.initialValue && !hasInteractedByUser) {
169 setValue(widget.initialValue);
170 }
171 }
172
173 @override
174 void dispose() {
175 _restorableController?.dispose();
176 super.dispose();
177 }
178
179 @override
180 void didChange(T? value) {
181 super.didChange(value);
182 _dropdownMenuFormField.onSelected?.call(value);
183 _updateRestorableController(value);
184 }
185
186 @override
187 void reset() {
188 super.reset();
189 _dropdownMenuFormField.onSelected?.call(value);
190 _updateRestorableController(widget.initialValue);
191 }
192
193 void _updateRestorableController(T? value) {
194 if (_restorableController != null) {
195 _restorableController!.value.value = TextEditingValue(text: _findLabelByValue(value));
196 }
197 }
198
199 @override
200 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
201 super.restoreState(oldBucket, initialRestore);
202 if (_restorableController != null) {
203 _registerRestorableController();
204 // Make sure to update the internal [DropdownMenuFieldState] value to sync up with
205 // text editing controller value if it matches one of the item label.
206 final T? matchingValue = _findValueByLabel(_restorableController!.value.text);
207 if (matchingValue != null) {
208 setValue(matchingValue);
209 }
210 }
211 }
212
213 void _registerRestorableController() {
214 assert(_restorableController != null);
215 registerForRestoration(_restorableController!, 'controller');
216 }
217
218 T? _findValueByLabel(String label) {
219 for (final DropdownMenuEntry<T> entry in _dropdownMenuFormField.dropdownMenuEntries) {
220 if (entry.label == label) {
221 return entry.value;
222 }
223 }
224 return null;
225 }
226
227 String _findLabelByValue(T? value) {
228 for (final DropdownMenuEntry<T> entry in _dropdownMenuFormField.dropdownMenuEntries) {
229 if (entry.value == value) {
230 return entry.label;
231 }
232 }
233 return '';
234 }
235}
236