text_form_field.dart 13.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import 'input_decorator.dart';
import 'text_field.dart';
10
import 'theme.dart';
11

12
export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType;
13

14 15
/// A [FormField] that contains a [TextField].
///
16
/// This is a convenience widget that wraps a [TextField] widget in a
17 18 19 20
/// [FormField].
///
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
/// save, reset, or validate multiple fields at once. To use without a [Form],
21 22
/// pass a `GlobalKey<FormFieldState>` (see [GlobalKey]) to the constructor and use
/// [GlobalKey.currentState] to save or reset the form field.
23
///
24 25 26 27 28 29 30 31 32
/// When a [controller] is specified, its [TextEditingController.text]
/// defines the [initialValue]. If this [FormField] is part of a scrolling
/// container that lazily constructs its children, like a [ListView] or a
/// [CustomScrollView], then a [controller] should be specified.
/// The controller's lifetime should be managed by a stateful widget ancestor
/// of the scrolling container.
///
/// If a [controller] is not specified, [initialValue] can be used to give
/// the automatically generated controller an initial value.
33
///
34 35
/// {@macro flutter.material.textfield.wantKeepAlive}
///
36
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
37 38
/// when it is no longer needed. This will ensure any resources used by the object
/// are discarded.
39
///
40 41 42
/// By default, `decoration` will apply the [ThemeData.inputDecorationTheme] for
/// the current context to the [InputDecoration], see
/// [InputDecoration.applyDefaults].
43
///
44 45
/// For a documentation about the various parameters, see [TextField].
///
46
/// {@tool snippet}
47 48 49
///
/// Creates a [TextFormField] with an [InputDecoration] and validator function.
///
50 51 52 53
/// ![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)
///
/// ![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)
///
54 55 56 57 58 59 60
/// ```dart
/// TextFormField(
///   decoration: const InputDecoration(
///     icon: Icon(Icons.person),
///     hintText: 'What do people call you?',
///     labelText: 'Name *',
///   ),
61
///   onSaved: (String? value) {
62 63 64
///     // This optional block of code can be used to run
///     // code when the user saves the form.
///   },
65 66
///   validator: (String? value) {
///     return (value != null && value.contains('@')) ? 'Do not use the @ char.' : null;
67 68 69
///   },
/// )
/// ```
70
/// {@end-tool}
71
///
72
/// {@tool dartpad}
73
/// This example shows how to move the focus to the next field when the user
74
/// presses the SPACE key.
75
///
76
/// ** See code in examples/api/lib/material/text_form_field/text_form_field.1.dart **
77 78
/// {@end-tool}
///
79 80
/// See also:
///
81
///  * <https://material.io/design/components/text-fields.html>
82 83 84 85
///  * [TextField], which is the underlying text field without the [Form]
///    integration.
///  * [InputDecorator], which shows the labels and other visual elements that
///    surround the actual text editing widget.
86
///  * Learn how to use a [TextEditingController] in one of our [cookbook recipes](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
87
class TextFormField extends FormField<String> {
88 89
  /// Creates a [FormField] that contains a [TextField].
  ///
90 91 92
  /// When a [controller] is specified, [initialValue] must be null (the
  /// default). If [controller] is null, then a [TextEditingController]
  /// will be constructed automatically and its `text` will be initialized
93
  /// to [initialValue] or the empty string.
94
  ///
95
  /// For documentation about the various parameters, see the [TextField] class
96
  /// and [TextField.new], the constructor.
97
  TextFormField({
98
    super.key,
99
    this.controller,
100 101 102 103
    String? initialValue,
    FocusNode? focusNode,
    InputDecoration? decoration = const InputDecoration(),
    TextInputType? keyboardType,
104
    TextCapitalization textCapitalization = TextCapitalization.none,
105 106 107 108
    TextInputAction? textInputAction,
    TextStyle? style,
    StrutStyle? strutStyle,
    TextDirection? textDirection,
109
    TextAlign textAlign = TextAlign.start,
110
    TextAlignVertical? textAlignVertical,
111
    bool autofocus = false,
112
    bool readOnly = false,
113 114
    ToolbarOptions? toolbarOptions,
    bool? showCursor,
115
    String obscuringCharacter = '•',
116 117
    bool obscureText = false,
    bool autocorrect = true,
118 119
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
120
    bool enableSuggestions = true,
121
    MaxLengthEnforcement? maxLengthEnforcement,
122 123
    int? maxLines = 1,
    int? minLines,
124
    bool expands = false,
125 126 127
    int? maxLength,
    ValueChanged<String>? onChanged,
    GestureTapCallback? onTap,
128
    TapRegionCallback? onTapOutside,
129 130
    VoidCallback? onEditingComplete,
    ValueChanged<String>? onFieldSubmitted,
131 132
    super.onSaved,
    super.validator,
133 134
    List<TextInputFormatter>? inputFormatters,
    bool? enabled,
135
    double cursorWidth = 2.0,
136 137 138 139
    double? cursorHeight,
    Radius? cursorRadius,
    Color? cursorColor,
    Brightness? keyboardAppearance,
140
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
141
    bool? enableInteractiveSelection,
142
    TextSelectionControls? selectionControls,
143 144 145 146
    InputCounterWidgetBuilder? buildCounter,
    ScrollPhysics? scrollPhysics,
    Iterable<String>? autofillHints,
    AutovalidateMode? autovalidateMode,
147
    ScrollController? scrollController,
148
    super.restorationId,
149
    bool enableIMEPersonalizedLearning = true,
150
    MouseCursor? mouseCursor,
151
  }) : assert(initialValue == null || controller == null),
152
       assert(textAlign != null),
153
       assert(autofocus != null),
154
       assert(readOnly != null),
155
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
156
       assert(obscureText != null),
157
       assert(autocorrect != null),
158
       assert(enableSuggestions != null),
159
       assert(scrollPadding != null),
160
       assert(maxLines == null || maxLines > 0),
161 162 163
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
164
         "minLines can't be greater than maxLines",
165 166 167 168 169 170
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
171
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
172
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
173
       assert(enableIMEPersonalizedLearning != null),
174
       super(
175 176
         initialValue: controller != null ? controller.text : (initialValue ?? ''),
         enabled: enabled ?? decoration?.enabled ?? true,
177
         autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
178 179 180 181 182 183 184 185 186
         builder: (FormFieldState<String> field) {
           final _TextFormFieldState state = field as _TextFormFieldState;
           final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
               .applyDefaults(Theme.of(field.context).inputDecorationTheme);
           void onChangedHandler(String value) {
             field.didChange(value);
             if (onChanged != null) {
               onChanged(value);
             }
187
           }
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
           return UnmanagedRestorationScope(
             bucket: field.bucket,
             child: TextField(
               restorationId: restorationId,
               controller: state._effectiveController,
               focusNode: focusNode,
               decoration: effectiveDecoration.copyWith(errorText: field.errorText),
               keyboardType: keyboardType,
               textInputAction: textInputAction,
               style: style,
               strutStyle: strutStyle,
               textAlign: textAlign,
               textAlignVertical: textAlignVertical,
               textDirection: textDirection,
               textCapitalization: textCapitalization,
               autofocus: autofocus,
               toolbarOptions: toolbarOptions,
               readOnly: readOnly,
               showCursor: showCursor,
               obscuringCharacter: obscuringCharacter,
               obscureText: obscureText,
               autocorrect: autocorrect,
               smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
               smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
               enableSuggestions: enableSuggestions,
               maxLengthEnforcement: maxLengthEnforcement,
               maxLines: maxLines,
               minLines: minLines,
               expands: expands,
               maxLength: maxLength,
               onChanged: onChangedHandler,
               onTap: onTap,
220
               onTapOutside: onTapOutside,
221 222 223 224 225 226 227 228 229 230 231
               onEditingComplete: onEditingComplete,
               onSubmitted: onFieldSubmitted,
               inputFormatters: inputFormatters,
               enabled: enabled ?? decoration?.enabled ?? true,
               cursorWidth: cursorWidth,
               cursorHeight: cursorHeight,
               cursorRadius: cursorRadius,
               cursorColor: cursorColor,
               scrollPadding: scrollPadding,
               scrollPhysics: scrollPhysics,
               keyboardAppearance: keyboardAppearance,
232
               enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
233 234 235 236
               selectionControls: selectionControls,
               buildCounter: buildCounter,
               autofillHints: autofillHints,
               scrollController: scrollController,
237
               enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
238
               mouseCursor: mouseCursor,
239 240 241 242
             ),
           );
         },
       );
243 244 245

  /// Controls the text being edited.
  ///
246 247
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
248
  final TextEditingController? controller;
249 250

  @override
251
  FormFieldState<String> createState() => _TextFormFieldState();
252 253 254
}

class _TextFormFieldState extends FormFieldState<String> {
255
  RestorableTextEditingController? _controller;
256

257
  TextEditingController get _effectiveController => _textFormField.controller ?? _controller!.value;
258

259
  TextFormField get _textFormField => super.widget as TextFormField;
260

261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    super.restoreState(oldBucket, initialRestore);
    if (_controller != null) {
      _registerController();
    }
    // Make sure to update the internal [FormFieldState] value to sync up with
    // text editing controller value.
    setValue(_effectiveController.text);
  }

  void _registerController() {
    assert(_controller != null);
    registerForRestoration(_controller!, 'controller');
  }

  void _createLocalController([TextEditingValue? value]) {
    assert(_controller == null);
    _controller = value == null
        ? RestorableTextEditingController()
        : RestorableTextEditingController.fromValue(value);
    if (!restorePending) {
      _registerController();
    }
  }

287 288 289
  @override
  void initState() {
    super.initState();
290
    if (_textFormField.controller == null) {
291
      _createLocalController(widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null);
292
    } else {
293
      _textFormField.controller!.addListener(_handleControllerChanged);
294 295 296 297 298 299
    }
  }

  @override
  void didUpdateWidget(TextFormField oldWidget) {
    super.didUpdateWidget(oldWidget);
300
    if (_textFormField.controller != oldWidget.controller) {
301
      oldWidget.controller?.removeListener(_handleControllerChanged);
302
      _textFormField.controller?.addListener(_handleControllerChanged);
303

304
      if (oldWidget.controller != null && _textFormField.controller == null) {
305 306 307
        _createLocalController(oldWidget.controller!.value);
      }

308 309
      if (_textFormField.controller != null) {
        setValue(_textFormField.controller!.text);
310 311 312
        if (oldWidget.controller == null) {
          unregisterFromRestoration(_controller!);
          _controller!.dispose();
313
          _controller = null;
314
        }
315 316 317 318 319 320
      }
    }
  }

  @override
  void dispose() {
321
    _textFormField.controller?.removeListener(_handleControllerChanged);
322
    _controller?.dispose();
323 324 325
    super.dispose();
  }

326
  @override
327
  void didChange(String? value) {
328 329
    super.didChange(value);

330
    if (_effectiveController.text != value) {
331
      _effectiveController.text = value ?? '';
332
    }
333 334
  }

335 336
  @override
  void reset() {
337 338
    // setState will be called in the superclass, so even though state is being
    // manipulated, no setState call is needed here.
339
    _effectiveController.text = widget.initialValue ?? '';
340 341 342 343 344 345 346 347 348 349 350
    super.reset();
  }

  void _handleControllerChanged() {
    // Suppress changes that originated from within this class.
    //
    // In the case where a controller has been passed in to this widget, we
    // register this change listener. In these cases, we'll also receive change
    // notifications for changes originating from within this class -- for
    // example, the reset() method. In such cases, the FormField value will
    // already have been set.
351
    if (_effectiveController.text != value) {
352
      didChange(_effectiveController.text);
353
    }
354
  }
355
}