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'; |
6 | library; |
7 | |
8 | import 'package:flutter/services.dart'; |
9 | import 'package:flutter/widgets.dart'; |
10 | |
11 | import 'dropdown_menu.dart'; |
12 | import '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. |
30 | class 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 | |
144 | class _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 | |