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
5import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/gestures.dart';
9import 'package:flutter/services.dart';
10import 'package:flutter/widgets.dart';
11
12import 'adaptive_text_selection_toolbar.dart';
13import 'input_decorator.dart';
14import 'material_state.dart';
15import 'text_field.dart';
16import 'theme.dart';
17
18export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType;
19
20/// A [FormField] that contains a [TextField].
21///
22/// This is a convenience widget that wraps a [TextField] widget in a
23/// [FormField].
24///
25/// A [Form] ancestor is not required. The [Form] allows one to
26/// save, reset, or validate multiple fields at once. To use without a [Form],
27/// pass a `GlobalKey<FormFieldState>` (see [GlobalKey]) to the constructor and use
28/// [GlobalKey.currentState] to save or reset the form field.
29///
30/// When a [controller] is specified, its [TextEditingController.text]
31/// defines the [initialValue]. If this [FormField] is part of a scrolling
32/// container that lazily constructs its children, like a [ListView] or a
33/// [CustomScrollView], then a [controller] should be specified.
34/// The controller's lifetime should be managed by a stateful widget ancestor
35/// of the scrolling container.
36///
37/// If a [controller] is not specified, [initialValue] can be used to give
38/// the automatically generated controller an initial value.
39///
40/// {@macro flutter.material.textfield.wantKeepAlive}
41///
42/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
43/// when it is no longer needed. This will ensure any resources used by the object
44/// are discarded.
45///
46/// By default, `decoration` will apply the [ThemeData.inputDecorationTheme] for
47/// the current context to the [InputDecoration], see
48/// [InputDecoration.applyDefaults].
49///
50/// For a documentation about the various parameters, see [TextField].
51///
52/// {@tool snippet}
53///
54/// Creates a [TextFormField] with an [InputDecoration] and validator function.
55///
56/// ![If the user enters valid text, the TextField appears normally without any warnings to the user](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field.png)
57///
58/// ![If the user enters invalid text, the error message returned from the validator function is displayed in dark red underneath the input](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field_error.png)
59///
60/// ```dart
61/// TextFormField(
62/// decoration: const InputDecoration(
63/// icon: Icon(Icons.person),
64/// hintText: 'What do people call you?',
65/// labelText: 'Name *',
66/// ),
67/// onSaved: (String? value) {
68/// // This optional block of code can be used to run
69/// // code when the user saves the form.
70/// },
71/// validator: (String? value) {
72/// return (value != null && value.contains('@')) ? 'Do not use the @ char.' : null;
73/// },
74/// )
75/// ```
76/// {@end-tool}
77///
78/// {@tool dartpad}
79/// This example shows how to move the focus to the next field when the user
80/// presses the SPACE key.
81///
82/// ** See code in examples/api/lib/material/text_form_field/text_form_field.1.dart **
83/// {@end-tool}
84///
85/// {@tool dartpad}
86/// This example shows how to force an error text to the field after making
87/// an asynchronous call.
88///
89/// ** See code in examples/api/lib/material/text_form_field/text_form_field.2.dart **
90/// {@end-tool}
91///
92/// See also:
93///
94/// * <https://material.io/design/components/text-fields.html>
95/// * [TextField], which is the underlying text field without the [Form]
96/// integration.
97/// * [InputDecorator], which shows the labels and other visual elements that
98/// surround the actual text editing widget.
99/// * Learn how to use a [TextEditingController] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
100class TextFormField extends FormField<String> {
101 /// Creates a [FormField] that contains a [TextField].
102 ///
103 /// When a [controller] is specified, [initialValue] must be null (the
104 /// default). If [controller] is null, then a [TextEditingController]
105 /// will be constructed automatically and its `text` will be initialized
106 /// to [initialValue] or the empty string.
107 ///
108 /// For documentation about the various parameters, see the [TextField] class
109 /// and [TextField.new], the constructor.
110 TextFormField({
111 super.key,
112 this.groupId = EditableText,
113 this.controller,
114 String? initialValue,
115 FocusNode? focusNode,
116 super.forceErrorText,
117 InputDecoration? decoration = const InputDecoration(),
118 TextInputType? keyboardType,
119 TextCapitalization textCapitalization = TextCapitalization.none,
120 TextInputAction? textInputAction,
121 TextStyle? style,
122 StrutStyle? strutStyle,
123 TextDirection? textDirection,
124 TextAlign textAlign = TextAlign.start,
125 TextAlignVertical? textAlignVertical,
126 bool autofocus = false,
127 bool readOnly = false,
128 @Deprecated(
129 'Use `contextMenuBuilder` instead. '
130 'This feature was deprecated after v3.3.0-0.5.pre.',
131 )
132 ToolbarOptions? toolbarOptions,
133 bool? showCursor,
134 String obscuringCharacter = '•',
135 bool obscureText = false,
136 bool autocorrect = true,
137 SmartDashesType? smartDashesType,
138 SmartQuotesType? smartQuotesType,
139 bool enableSuggestions = true,
140 MaxLengthEnforcement? maxLengthEnforcement,
141 int? maxLines = 1,
142 int? minLines,
143 bool expands = false,
144 int? maxLength,
145 this.onChanged,
146 GestureTapCallback? onTap,
147 bool onTapAlwaysCalled = false,
148 TapRegionCallback? onTapOutside,
149 TapRegionUpCallback? onTapUpOutside,
150 VoidCallback? onEditingComplete,
151 ValueChanged<String>? onFieldSubmitted,
152 super.onSaved,
153 super.validator,
154 super.errorBuilder,
155 List<TextInputFormatter>? inputFormatters,
156 bool? enabled,
157 bool? ignorePointers,
158 double cursorWidth = 2.0,
159 double? cursorHeight,
160 Radius? cursorRadius,
161 Color? cursorColor,
162 Color? cursorErrorColor,
163 Brightness? keyboardAppearance,
164 EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
165 bool? enableInteractiveSelection,
166 bool? selectAllOnFocus,
167 TextSelectionControls? selectionControls,
168 InputCounterWidgetBuilder? buildCounter,
169 ScrollPhysics? scrollPhysics,
170 Iterable<String>? autofillHints,
171 AutovalidateMode? autovalidateMode,
172 ScrollController? scrollController,
173 super.restorationId,
174 bool enableIMEPersonalizedLearning = true,
175 MouseCursor? mouseCursor,
176 EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder,
177 SpellCheckConfiguration? spellCheckConfiguration,
178 TextMagnifierConfiguration? magnifierConfiguration,
179 UndoHistoryController? undoController,
180 AppPrivateCommandCallback? onAppPrivateCommand,
181 bool? cursorOpacityAnimates,
182 ui.BoxHeightStyle? selectionHeightStyle,
183 ui.BoxWidthStyle? selectionWidthStyle,
184 DragStartBehavior dragStartBehavior = DragStartBehavior.start,
185 ContentInsertionConfiguration? contentInsertionConfiguration,
186 MaterialStatesController? statesController,
187 Clip clipBehavior = Clip.hardEdge,
188 @Deprecated(
189 'Use `stylusHandwritingEnabled` instead. '
190 'This feature was deprecated after v3.27.0-0.2.pre.',
191 )
192 bool scribbleEnabled = true,
193 bool stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled,
194 bool canRequestFocus = true,
195 List<Locale>? hintLocales,
196 }) : assert(initialValue == null || controller == null),
197 assert(obscuringCharacter.length == 1),
198 assert(maxLines == null || maxLines > 0),
199 assert(minLines == null || minLines > 0),
200 assert(
201 (maxLines == null) || (minLines == null) || (maxLines >= minLines),
202 "minLines can't be greater than maxLines",
203 ),
204 assert(
205 !expands || (maxLines == null && minLines == null),
206 'minLines and maxLines must be null when expands is true.',
207 ),
208 assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
209 assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
210 super(
211 initialValue: controller != null ? controller.text : (initialValue ?? ''),
212 enabled: enabled ?? decoration?.enabled ?? true,
213 autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
214 builder: (FormFieldState<String> field) {
215 final _TextFormFieldState state = field as _TextFormFieldState;
216 InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
217 .applyDefaults(Theme.of(field.context).inputDecorationTheme);
218
219 final String? errorText = field.errorText;
220 if (errorText != null) {
221 effectiveDecoration = errorBuilder != null
222 ? effectiveDecoration.copyWith(error: errorBuilder(state.context, errorText))
223 : effectiveDecoration.copyWith(errorText: errorText);
224 }
225
226 void onChangedHandler(String value) {
227 field.didChange(value);
228 onChanged?.call(value);
229 }
230
231 return UnmanagedRestorationScope(
232 bucket: field.bucket,
233 child: TextField(
234 groupId: groupId,
235 restorationId: restorationId,
236 controller: state._effectiveController,
237 focusNode: focusNode,
238 decoration: effectiveDecoration,
239 keyboardType: keyboardType,
240 textInputAction: textInputAction,
241 style: style,
242 strutStyle: strutStyle,
243 textAlign: textAlign,
244 textAlignVertical: textAlignVertical,
245 textDirection: textDirection,
246 textCapitalization: textCapitalization,
247 autofocus: autofocus,
248 statesController: statesController,
249 toolbarOptions: toolbarOptions,
250 readOnly: readOnly,
251 showCursor: showCursor,
252 obscuringCharacter: obscuringCharacter,
253 obscureText: obscureText,
254 autocorrect: autocorrect,
255 smartDashesType:
256 smartDashesType ??
257 (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
258 smartQuotesType:
259 smartQuotesType ??
260 (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
261 enableSuggestions: enableSuggestions,
262 maxLengthEnforcement: maxLengthEnforcement,
263 maxLines: maxLines,
264 minLines: minLines,
265 expands: expands,
266 maxLength: maxLength,
267 onChanged: onChangedHandler,
268 onTap: onTap,
269 onTapAlwaysCalled: onTapAlwaysCalled,
270 onTapOutside: onTapOutside,
271 onTapUpOutside: onTapUpOutside,
272 onEditingComplete: onEditingComplete,
273 onSubmitted: onFieldSubmitted,
274 inputFormatters: inputFormatters,
275 enabled: enabled ?? decoration?.enabled ?? true,
276 ignorePointers: ignorePointers,
277 cursorWidth: cursorWidth,
278 cursorHeight: cursorHeight,
279 cursorRadius: cursorRadius,
280 cursorColor: cursorColor,
281 cursorErrorColor: cursorErrorColor,
282 scrollPadding: scrollPadding,
283 scrollPhysics: scrollPhysics,
284 keyboardAppearance: keyboardAppearance,
285 enableInteractiveSelection:
286 enableInteractiveSelection ?? (!obscureText || !readOnly),
287 selectAllOnFocus: selectAllOnFocus,
288 selectionControls: selectionControls,
289 buildCounter: buildCounter,
290 autofillHints: autofillHints,
291 scrollController: scrollController,
292 enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
293 mouseCursor: mouseCursor,
294 contextMenuBuilder: contextMenuBuilder,
295 spellCheckConfiguration: spellCheckConfiguration,
296 magnifierConfiguration: magnifierConfiguration,
297 undoController: undoController,
298 onAppPrivateCommand: onAppPrivateCommand,
299 cursorOpacityAnimates: cursorOpacityAnimates,
300 selectionHeightStyle:
301 selectionHeightStyle ?? EditableText.defaultSelectionHeightStyle,
302 selectionWidthStyle: selectionWidthStyle ?? EditableText.defaultSelectionWidthStyle,
303 dragStartBehavior: dragStartBehavior,
304 contentInsertionConfiguration: contentInsertionConfiguration,
305 clipBehavior: clipBehavior,
306 scribbleEnabled: scribbleEnabled,
307 stylusHandwritingEnabled: stylusHandwritingEnabled,
308 canRequestFocus: canRequestFocus,
309 hintLocales: hintLocales,
310 ),
311 );
312 },
313 );
314
315 /// Controls the text being edited.
316 ///
317 /// If null, this widget will create its own [TextEditingController] and
318 /// initialize its [TextEditingController.text] with [initialValue].
319 final TextEditingController? controller;
320
321 /// {@macro flutter.widgets.editableText.groupId}
322 final Object groupId;
323
324 /// {@template flutter.material.TextFormField.onChanged}
325 /// Called when the user initiates a change to the TextField's
326 /// value: when they have inserted or deleted text or reset the form.
327 /// {@endtemplate}
328 final ValueChanged<String>? onChanged;
329
330 static Widget _defaultContextMenuBuilder(
331 BuildContext context,
332 EditableTextState editableTextState,
333 ) {
334 if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) {
335 return SystemContextMenu.editableText(editableTextState: editableTextState);
336 }
337 return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState);
338 }
339
340 @override
341 FormFieldState<String> createState() => _TextFormFieldState();
342}
343
344class _TextFormFieldState extends FormFieldState<String> {
345 RestorableTextEditingController? _controller;
346
347 TextEditingController get _effectiveController => _textFormField.controller ?? _controller!.value;
348
349 TextFormField get _textFormField => super.widget as TextFormField;
350
351 @override
352 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
353 super.restoreState(oldBucket, initialRestore);
354 if (_controller != null) {
355 _registerController();
356 }
357 // Make sure to update the internal [FormFieldState] value to sync up with
358 // text editing controller value.
359 setValue(_effectiveController.text);
360 }
361
362 void _registerController() {
363 assert(_controller != null);
364 registerForRestoration(_controller!, 'controller');
365 }
366
367 void _createLocalController([TextEditingValue? value]) {
368 assert(_controller == null);
369 _controller = value == null
370 ? RestorableTextEditingController()
371 : RestorableTextEditingController.fromValue(value);
372 if (!restorePending) {
373 _registerController();
374 }
375 }
376
377 @override
378 void initState() {
379 super.initState();
380 if (_textFormField.controller == null) {
381 _createLocalController(
382 widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null,
383 );
384 } else {
385 _textFormField.controller!.addListener(_handleControllerChanged);
386 }
387 }
388
389 @override
390 void didUpdateWidget(TextFormField oldWidget) {
391 super.didUpdateWidget(oldWidget);
392 if (_textFormField.controller != oldWidget.controller) {
393 oldWidget.controller?.removeListener(_handleControllerChanged);
394 _textFormField.controller?.addListener(_handleControllerChanged);
395
396 if (oldWidget.controller != null && _textFormField.controller == null) {
397 _createLocalController(oldWidget.controller!.value);
398 }
399
400 if (_textFormField.controller != null) {
401 setValue(_textFormField.controller!.text);
402 if (oldWidget.controller == null) {
403 unregisterFromRestoration(_controller!);
404 _controller!.dispose();
405 _controller = null;
406 }
407 }
408 }
409 }
410
411 @override
412 void dispose() {
413 _textFormField.controller?.removeListener(_handleControllerChanged);
414 _controller?.dispose();
415 super.dispose();
416 }
417
418 @override
419 void didChange(String? value) {
420 super.didChange(value);
421
422 if (_effectiveController.text != value) {
423 _effectiveController.value = TextEditingValue(text: value ?? '');
424 }
425 }
426
427 @override
428 void reset() {
429 // Set the controller value before calling super.reset() to let
430 // _handleControllerChanged suppress the change.
431 _effectiveController.value = TextEditingValue(text: widget.initialValue ?? '');
432 super.reset();
433 _textFormField.onChanged?.call(_effectiveController.text);
434 }
435
436 void _handleControllerChanged() {
437 // Suppress changes that originated from within this class.
438 //
439 // In the case where a controller has been passed in to this widget, we
440 // register this change listener. In these cases, we'll also receive change
441 // notifications for changes originating from within this class -- for
442 // example, the reset() method. In such cases, the FormField value will
443 // already have been set.
444 if (_effectiveController.text != value) {
445 didChange(_effectiveController.text);
446 }
447 }
448}
449