text_form_field.dart 14.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 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 36
/// 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.
37
///
38 39 40
/// By default, `decoration` will apply the [ThemeData.inputDecorationTheme] for
/// the current context to the [InputDecoration], see
/// [InputDecoration.applyDefaults].
41
///
42 43
/// For a documentation about the various parameters, see [TextField].
///
44
/// {@tool snippet}
45 46 47
///
/// Creates a [TextFormField] with an [InputDecoration] and validator function.
///
48 49 50 51
/// ![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)
///
52 53 54 55 56 57 58
/// ```dart
/// TextFormField(
///   decoration: const InputDecoration(
///     icon: Icon(Icons.person),
///     hintText: 'What do people call you?',
///     labelText: 'Name *',
///   ),
59
///   onSaved: (String? value) {
60 61 62
///     // This optional block of code can be used to run
///     // code when the user saves the form.
///   },
63 64
///   validator: (String? value) {
///     return (value != null && value.contains('@')) ? 'Do not use the @ char.' : null;
65 66 67
///   },
/// )
/// ```
68
/// {@end-tool}
69
///
70
/// {@tool dartpad --template=stateful_widget_material}
71
/// This example shows how to move the focus to the next field when the user
72
/// presses the SPACE key.
73
///
74
/// ** See code in examples/api/lib/material/text_form_field/text_form_field.1.dart **
75 76
/// {@end-tool}
///
77 78
/// See also:
///
79
///  * <https://material.io/design/components/text-fields.html>
80 81 82 83
///  * [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.
84
///  * 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).
85
class TextFormField extends FormField<String> {
86 87
  /// Creates a [FormField] that contains a [TextField].
  ///
88 89 90
  /// 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
91
  /// to [initialValue] or the empty string.
92
  ///
93 94
  /// For documentation about the various parameters, see the [TextField] class
  /// and [new TextField], the constructor.
95
  TextFormField({
96
    Key? key,
97
    this.controller,
98 99 100 101
    String? initialValue,
    FocusNode? focusNode,
    InputDecoration? decoration = const InputDecoration(),
    TextInputType? keyboardType,
102
    TextCapitalization textCapitalization = TextCapitalization.none,
103 104 105 106
    TextInputAction? textInputAction,
    TextStyle? style,
    StrutStyle? strutStyle,
    TextDirection? textDirection,
107
    TextAlign textAlign = TextAlign.start,
108
    TextAlignVertical? textAlignVertical,
109
    bool autofocus = false,
110
    bool readOnly = false,
111 112
    ToolbarOptions? toolbarOptions,
    bool? showCursor,
113
    String obscuringCharacter = '•',
114 115
    bool obscureText = false,
    bool autocorrect = true,
116 117
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
118
    bool enableSuggestions = true,
119
    @Deprecated(
120
      'Use autovalidateMode parameter which provide more specific '
121
      'behaviour related to auto validation. '
122
      'This feature was deprecated after v1.19.0.',
123
    )
124
    bool autovalidate = false,
125 126 127
    @Deprecated(
      'Use maxLengthEnforcement parameter which provides more specific '
      'behavior related to the maxLength limit. '
128
      'This feature was deprecated after v1.25.0-5.0.pre.',
129
    )
130
    bool maxLengthEnforced = true,
131
    MaxLengthEnforcement? maxLengthEnforcement,
132 133
    int? maxLines = 1,
    int? minLines,
134
    bool expands = false,
135 136 137 138 139 140 141 142 143
    int? maxLength,
    ValueChanged<String>? onChanged,
    GestureTapCallback? onTap,
    VoidCallback? onEditingComplete,
    ValueChanged<String>? onFieldSubmitted,
    FormFieldSetter<String>? onSaved,
    FormFieldValidator<String>? validator,
    List<TextInputFormatter>? inputFormatters,
    bool? enabled,
144
    double cursorWidth = 2.0,
145 146 147 148
    double? cursorHeight,
    Radius? cursorRadius,
    Color? cursorColor,
    Brightness? keyboardAppearance,
149
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
150
    bool enableInteractiveSelection = true,
151
    TextSelectionControls? selectionControls,
152 153 154 155
    InputCounterWidgetBuilder? buildCounter,
    ScrollPhysics? scrollPhysics,
    Iterable<String>? autofillHints,
    AutovalidateMode? autovalidateMode,
156
    ScrollController? scrollController,
157
    String? restorationId,
158
    bool enableIMEPersonalizedLearning = true,
159
  }) : assert(initialValue == null || controller == null),
160
       assert(textAlign != null),
161
       assert(autofocus != null),
162
       assert(readOnly != null),
163
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
164
       assert(obscureText != null),
165
       assert(autocorrect != null),
166
       assert(enableSuggestions != null),
167
       assert(autovalidate != null),
168 169 170
       assert(
         autovalidate == false ||
         autovalidate == true && autovalidateMode == null,
171
         'autovalidate and autovalidateMode should not be used together.',
172
       ),
173
       assert(maxLengthEnforced != null),
174 175 176 177
       assert(
         maxLengthEnforced || maxLengthEnforcement == null,
         'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
       ),
178
       assert(scrollPadding != null),
179
       assert(maxLines == null || maxLines > 0),
180 181 182
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
183
         "minLines can't be greater than maxLines",
184 185 186 187 188 189
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
190
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
191
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
192
       assert(enableInteractiveSelection != null),
193
       assert(enableIMEPersonalizedLearning != null),
194
       super(
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
         key: key,
         restorationId: restorationId,
         initialValue: controller != null ? controller.text : (initialValue ?? ''),
         onSaved: onSaved,
         validator: validator,
         enabled: enabled ?? decoration?.enabled ?? true,
         autovalidateMode: autovalidate
             ? AutovalidateMode.always
             : (autovalidateMode ?? AutovalidateMode.disabled),
         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);
             }
213
           }
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
           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,
               maxLengthEnforced: maxLengthEnforced,
               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,
               enableInteractiveSelection: enableInteractiveSelection,
               selectionControls: selectionControls,
               buildCounter: buildCounter,
               autofillHints: autofillHints,
               scrollController: scrollController,
263
               enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
264 265 266 267
             ),
           );
         },
       );
268 269 270

  /// Controls the text being edited.
  ///
271 272
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
273
  final TextEditingController? controller;
274 275

  @override
276
  FormFieldState<String> createState() => _TextFormFieldState();
277 278 279
}

class _TextFormFieldState extends FormFieldState<String> {
280
  RestorableTextEditingController? _controller;
281

282
  TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
283 284

  @override
285
  TextFormField get widget => super.widget as TextFormField;
286

287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
  @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();
    }
  }

313 314 315 316
  @override
  void initState() {
    super.initState();
    if (widget.controller == null) {
317
      _createLocalController(widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null);
318
    } else {
319
      widget.controller!.addListener(_handleControllerChanged);
320 321 322 323 324 325 326 327 328 329
    }
  }

  @override
  void didUpdateWidget(TextFormField oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller) {
      oldWidget.controller?.removeListener(_handleControllerChanged);
      widget.controller?.addListener(_handleControllerChanged);

330 331 332 333
      if (oldWidget.controller != null && widget.controller == null) {
        _createLocalController(oldWidget.controller!.value);
      }

334
      if (widget.controller != null) {
335
        setValue(widget.controller!.text);
336 337 338
        if (oldWidget.controller == null) {
          unregisterFromRestoration(_controller!);
          _controller!.dispose();
339
          _controller = null;
340
        }
341 342 343 344 345 346 347
      }
    }
  }

  @override
  void dispose() {
    widget.controller?.removeListener(_handleControllerChanged);
348
    _controller?.dispose();
349 350 351
    super.dispose();
  }

352
  @override
353
  void didChange(String? value) {
354 355
    super.didChange(value);

356 357
    if (_effectiveController.text != value)
      _effectiveController.text = value ?? '';
358 359
  }

360 361
  @override
  void reset() {
362 363
    // setState will be called in the superclass, so even though state is being
    // manipulated, no setState call is needed here.
364
    _effectiveController.text = widget.initialValue ?? '';
365 366 367 368 369 370 371 372 373 374 375
    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.
376 377
    if (_effectiveController.text != value)
      didChange(_effectiveController.text);
378
  }
379
}