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 13
export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;

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 21 22 23
/// [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],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
///
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 37 38
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
/// when it is no longer needed. This will ensure we discard any resources used
/// by the object.
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
    Key? 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 130 131 132 133
    int? maxLength,
    ValueChanged<String>? onChanged,
    GestureTapCallback? onTap,
    VoidCallback? onEditingComplete,
    ValueChanged<String>? onFieldSubmitted,
    FormFieldSetter<String>? onSaved,
    FormFieldValidator<String>? validator,
    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
    String? 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 176 177 178 179
         key: key,
         restorationId: restorationId,
         initialValue: controller != null ? controller.text : (initialValue ?? ''),
         onSaved: onSaved,
         validator: validator,
         enabled: enabled ?? decoration?.enabled ?? true,
180
         autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
181 182 183 184 185 186 187 188 189
         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);
             }
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 230 231 232 233
           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,
234
               enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
235 236 237 238
               selectionControls: selectionControls,
               buildCounter: buildCounter,
               autofillHints: autofillHints,
               scrollController: scrollController,
239
               enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
240
               mouseCursor: mouseCursor,
241 242 243 244
             ),
           );
         },
       );
245 246 247

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

  @override
253
  FormFieldState<String> createState() => _TextFormFieldState();
254 255 256
}

class _TextFormFieldState extends FormFieldState<String> {
257
  RestorableTextEditingController? _controller;
258

259
  TextEditingController get _effectiveController => _textFormField.controller ?? _controller!.value;
260

261
  TextFormField get _textFormField => super.widget as TextFormField;
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 287 288
  @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();
    }
  }

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

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

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

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

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

328
  @override
329
  void didChange(String? value) {
330 331
    super.didChange(value);

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

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