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 'date_picker.dart'; |
6 | /// @docImport 'text_field.dart'; |
7 | library; |
8 | |
9 | import 'package:flutter/widgets.dart'; |
10 | |
11 | import 'date.dart'; |
12 | import 'date_picker_theme.dart'; |
13 | import 'input_border.dart'; |
14 | import 'input_decorator.dart'; |
15 | import 'material_localizations.dart'; |
16 | import 'text_form_field.dart'; |
17 | import 'theme.dart'; |
18 | |
19 | /// A [TextFormField] configured to accept and validate a date entered by a user. |
20 | /// |
21 | /// When the field is saved or submitted, the text will be parsed into a |
22 | /// [DateTime] according to the ambient locale's compact date format. If the |
23 | /// input text doesn't parse into a date, the [errorFormatText] message will |
24 | /// be displayed under the field. |
25 | /// |
26 | /// [firstDate], [lastDate], and [selectableDayPredicate] provide constraints on |
27 | /// what days are valid. If the input date isn't in the date range or doesn't pass |
28 | /// the given predicate, then the [errorInvalidText] message will be displayed |
29 | /// under the field. |
30 | /// |
31 | /// See also: |
32 | /// |
33 | /// * [showDatePicker], which shows a dialog that contains a Material Design |
34 | /// date picker which includes support for text entry of dates. |
35 | /// * [MaterialLocalizations.parseCompactDate], which is used to parse the text |
36 | /// input into a [DateTime]. |
37 | /// |
38 | class InputDatePickerFormField extends StatefulWidget { |
39 | /// Creates a [TextFormField] configured to accept and validate a date. |
40 | /// |
41 | /// If the optional [initialDate] is provided, then it will be used to populate |
42 | /// the text field. If the [fieldHintText] is provided, it will be shown. |
43 | /// |
44 | /// If [initialDate] is provided, it must not be before [firstDate] or after |
45 | /// [lastDate]. If [selectableDayPredicate] is provided, it must return `true` |
46 | /// for [initialDate]. |
47 | /// |
48 | /// [firstDate] must be on or before [lastDate]. |
49 | InputDatePickerFormField({ |
50 | super.key, |
51 | DateTime? initialDate, |
52 | required DateTime firstDate, |
53 | required DateTime lastDate, |
54 | this.onDateSubmitted, |
55 | this.onDateSaved, |
56 | this.selectableDayPredicate, |
57 | this.errorFormatText, |
58 | this.errorInvalidText, |
59 | this.fieldHintText, |
60 | this.fieldLabelText, |
61 | this.keyboardType, |
62 | this.autofocus = false, |
63 | this.acceptEmptyDate = false, |
64 | this.focusNode, |
65 | }) : initialDate = initialDate != null ? DateUtils.dateOnly(initialDate) : null, |
66 | firstDate = DateUtils.dateOnly(firstDate), |
67 | lastDate = DateUtils.dateOnly(lastDate) { |
68 | assert( |
69 | !this.lastDate.isBefore(this.firstDate), |
70 | 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.' , |
71 | ); |
72 | assert( |
73 | initialDate == null || !this.initialDate!.isBefore(this.firstDate), |
74 | 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.' , |
75 | ); |
76 | assert( |
77 | initialDate == null || !this.initialDate!.isAfter(this.lastDate), |
78 | 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.' , |
79 | ); |
80 | assert( |
81 | selectableDayPredicate == null || initialDate == null || selectableDayPredicate!(this.initialDate!), |
82 | 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.' , |
83 | ); |
84 | } |
85 | |
86 | /// If provided, it will be used as the default value of the field. |
87 | final DateTime? initialDate; |
88 | |
89 | /// The earliest allowable [DateTime] that the user can input. |
90 | final DateTime firstDate; |
91 | |
92 | /// The latest allowable [DateTime] that the user can input. |
93 | final DateTime lastDate; |
94 | |
95 | /// An optional method to call when the user indicates they are done editing |
96 | /// the text in the field. Will only be called if the input represents a valid |
97 | /// [DateTime]. |
98 | final ValueChanged<DateTime>? onDateSubmitted; |
99 | |
100 | /// An optional method to call with the final date when the form is |
101 | /// saved via [FormState.save]. Will only be called if the input represents |
102 | /// a valid [DateTime]. |
103 | final ValueChanged<DateTime>? onDateSaved; |
104 | |
105 | /// Function to provide full control over which [DateTime] can be selected. |
106 | final SelectableDayPredicate? selectableDayPredicate; |
107 | |
108 | /// The error text displayed if the entered date is not in the correct format. |
109 | final String? errorFormatText; |
110 | |
111 | /// The error text displayed if the date is not valid. |
112 | /// |
113 | /// A date is not valid if it is earlier than [firstDate], later than |
114 | /// [lastDate], or doesn't pass the [selectableDayPredicate]. |
115 | final String? errorInvalidText; |
116 | |
117 | /// The hint text displayed in the [TextField]. |
118 | /// |
119 | /// If this is null, it will default to the date format string. For example, |
120 | /// 'mm/dd/yyyy' for en_US. |
121 | final String? fieldHintText; |
122 | |
123 | /// The label text displayed in the [TextField]. |
124 | /// |
125 | /// If this is null, it will default to the words representing the date format |
126 | /// string. For example, 'Month, Day, Year' for en_US. |
127 | final String? fieldLabelText; |
128 | |
129 | /// The keyboard type of the [TextField]. |
130 | /// |
131 | /// If this is null, it will default to [TextInputType.datetime] |
132 | final TextInputType? keyboardType; |
133 | |
134 | /// {@macro flutter.widgets.editableText.autofocus} |
135 | final bool autofocus; |
136 | |
137 | /// Determines if an empty date would show [errorFormatText] or not. |
138 | /// |
139 | /// Defaults to false. |
140 | /// |
141 | /// If true, [errorFormatText] is not shown when the date input field is empty. |
142 | final bool acceptEmptyDate; |
143 | |
144 | /// {@macro flutter.widgets.Focus.focusNode} |
145 | final FocusNode? focusNode; |
146 | |
147 | @override |
148 | State<InputDatePickerFormField> createState() => _InputDatePickerFormFieldState(); |
149 | } |
150 | |
151 | class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> { |
152 | final TextEditingController _controller = TextEditingController(); |
153 | DateTime? _selectedDate; |
154 | String? _inputText; |
155 | bool _autoSelected = false; |
156 | |
157 | @override |
158 | void initState() { |
159 | super.initState(); |
160 | _selectedDate = widget.initialDate; |
161 | } |
162 | |
163 | @override |
164 | void dispose() { |
165 | _controller.dispose(); |
166 | super.dispose(); |
167 | } |
168 | |
169 | @override |
170 | void didChangeDependencies() { |
171 | super.didChangeDependencies(); |
172 | _updateValueForSelectedDate(); |
173 | } |
174 | |
175 | @override |
176 | void didUpdateWidget(InputDatePickerFormField oldWidget) { |
177 | super.didUpdateWidget(oldWidget); |
178 | if (widget.initialDate != oldWidget.initialDate) { |
179 | // Can't update the form field in the middle of a build, so do it next frame |
180 | WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { |
181 | setState(() { |
182 | _selectedDate = widget.initialDate; |
183 | _updateValueForSelectedDate(); |
184 | }); |
185 | }, debugLabel: 'InputDatePickerFormField.update' ); |
186 | } |
187 | } |
188 | |
189 | void _updateValueForSelectedDate() { |
190 | if (_selectedDate != null) { |
191 | final MaterialLocalizations localizations = MaterialLocalizations.of(context); |
192 | _inputText = localizations.formatCompactDate(_selectedDate!); |
193 | TextEditingValue textEditingValue = TextEditingValue(text: _inputText!); |
194 | // Select the new text if we are auto focused and haven't selected the text before. |
195 | if (widget.autofocus && !_autoSelected) { |
196 | textEditingValue = textEditingValue.copyWith(selection: TextSelection( |
197 | baseOffset: 0, |
198 | extentOffset: _inputText!.length, |
199 | )); |
200 | _autoSelected = true; |
201 | } |
202 | _controller.value = textEditingValue; |
203 | } else { |
204 | _inputText = '' ; |
205 | _controller.value = TextEditingValue(text: _inputText!); |
206 | } |
207 | } |
208 | |
209 | DateTime? _parseDate(String? text) { |
210 | final MaterialLocalizations localizations = MaterialLocalizations.of(context); |
211 | return localizations.parseCompactDate(text); |
212 | } |
213 | |
214 | bool _isValidAcceptableDate(DateTime? date) { |
215 | return |
216 | date != null && |
217 | !date.isBefore(widget.firstDate) && |
218 | !date.isAfter(widget.lastDate) && |
219 | (widget.selectableDayPredicate == null || widget.selectableDayPredicate!(date)); |
220 | } |
221 | |
222 | String? _validateDate(String? text) { |
223 | if ((text == null || text.isEmpty) && widget.acceptEmptyDate) { |
224 | return null; |
225 | } |
226 | final DateTime? date = _parseDate(text); |
227 | if (date == null) { |
228 | return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel; |
229 | } else if (!_isValidAcceptableDate(date)) { |
230 | return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel; |
231 | } |
232 | return null; |
233 | } |
234 | |
235 | void _updateDate(String? text, ValueChanged<DateTime>? callback) { |
236 | final DateTime? date = _parseDate(text); |
237 | if (_isValidAcceptableDate(date)) { |
238 | _selectedDate = date; |
239 | _inputText = text; |
240 | callback?.call(_selectedDate!); |
241 | } |
242 | } |
243 | |
244 | void _handleSaved(String? text) { |
245 | _updateDate(text, widget.onDateSaved); |
246 | } |
247 | |
248 | void _handleSubmitted(String text) { |
249 | _updateDate(text, widget.onDateSubmitted); |
250 | } |
251 | |
252 | @override |
253 | Widget build(BuildContext context) { |
254 | final ThemeData theme = Theme.of(context); |
255 | final bool useMaterial3 = theme.useMaterial3; |
256 | final MaterialLocalizations localizations = MaterialLocalizations.of(context); |
257 | final DatePickerThemeData datePickerTheme = theme.datePickerTheme; |
258 | final InputDecorationTheme inputTheme = theme.inputDecorationTheme; |
259 | final InputBorder effectiveInputBorder = datePickerTheme.inputDecorationTheme?.border |
260 | ?? theme.inputDecorationTheme.border |
261 | ?? (useMaterial3 ? const OutlineInputBorder() : const UnderlineInputBorder()); |
262 | |
263 | return Semantics( |
264 | container: true, |
265 | child: TextFormField( |
266 | decoration: InputDecoration( |
267 | hintText: widget.fieldHintText ?? localizations.dateHelpText, |
268 | labelText: widget.fieldLabelText ?? localizations.dateInputLabel, |
269 | ).applyDefaults(inputTheme |
270 | .merge(datePickerTheme.inputDecorationTheme) |
271 | .copyWith(border: effectiveInputBorder), |
272 | ), |
273 | validator: _validateDate, |
274 | keyboardType: widget.keyboardType ?? TextInputType.datetime, |
275 | onSaved: _handleSaved, |
276 | onFieldSubmitted: _handleSubmitted, |
277 | autofocus: widget.autofocus, |
278 | controller: _controller, |
279 | focusNode: widget.focusNode, |
280 | ), |
281 | ); |
282 | } |
283 | } |
284 | |