text_form_field.dart 13.4 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 128 129
    int? maxLength,
    ValueChanged<String>? onChanged,
    GestureTapCallback? onTap,
    VoidCallback? onEditingComplete,
    ValueChanged<String>? onFieldSubmitted,
130 131
    super.onSaved,
    super.validator,
132 133
    List<TextInputFormatter>? inputFormatters,
    bool? enabled,
134
    double cursorWidth = 2.0,
135 136 137 138
    double? cursorHeight,
    Radius? cursorRadius,
    Color? cursorColor,
    Brightness? keyboardAppearance,
139
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
140
    bool? enableInteractiveSelection,
141
    TextSelectionControls? selectionControls,
142 143 144 145
    InputCounterWidgetBuilder? buildCounter,
    ScrollPhysics? scrollPhysics,
    Iterable<String>? autofillHints,
    AutovalidateMode? autovalidateMode,
146
    ScrollController? scrollController,
147
    super.restorationId,
148
    bool enableIMEPersonalizedLearning = true,
149
    MouseCursor? mouseCursor,
150
  }) : assert(initialValue == null || controller == null),
151
       assert(textAlign != null),
152
       assert(autofocus != null),
153
       assert(readOnly != null),
154
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
155
       assert(obscureText != null),
156
       assert(autocorrect != null),
157
       assert(enableSuggestions != null),
158
       assert(scrollPadding != null),
159
       assert(maxLines == null || maxLines > 0),
160 161 162
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
163
         "minLines can't be greater than maxLines",
164 165 166 167 168 169
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
170
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
171
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
172
       assert(enableIMEPersonalizedLearning != null),
173
       super(
174 175
         initialValue: controller != null ? controller.text : (initialValue ?? ''),
         enabled: enabled ?? decoration?.enabled ?? true,
176
         autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
177 178 179 180 181 182 183 184 185
         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);
             }
186
           }
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 220 221 222 223 224 225 226 227 228 229
           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,
               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,
230
               enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
231 232 233 234
               selectionControls: selectionControls,
               buildCounter: buildCounter,
               autofillHints: autofillHints,
               scrollController: scrollController,
235
               enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
236
               mouseCursor: mouseCursor,
237 238 239 240
             ),
           );
         },
       );
241 242 243

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

  @override
249
  FormFieldState<String> createState() => _TextFormFieldState();
250 251 252
}

class _TextFormFieldState extends FormFieldState<String> {
253
  RestorableTextEditingController? _controller;
254

255
  TextEditingController get _effectiveController => _textFormField.controller ?? _controller!.value;
256

257
  TextFormField get _textFormField => super.widget as TextFormField;
258

259 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
  @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();
    }
  }

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

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

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

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

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

324
  @override
325
  void didChange(String? value) {
326 327
    super.didChange(value);

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

333 334
  @override
  void reset() {
335 336
    // setState will be called in the superclass, so even though state is being
    // manipulated, no setState call is needed here.
337
    _effectiveController.text = widget.initialValue ?? '';
338 339 340 341 342 343 344 345 346 347 348
    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.
349
    if (_effectiveController.text != value) {
350
      didChange(_effectiveController.text);
351
    }
352
  }
353
}