text_form_field.dart 14.1 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 75 76 77 78 79 80 81 82 83
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Material(
///     child: Center(
///       child: Shortcuts(
///         shortcuts: <LogicalKeySet, Intent>{
84 85
///           // Pressing space in the field will now move to the next field.
///           LogicalKeySet(LogicalKeyboardKey.space): const NextFocusIntent(),
86 87 88
///         },
///         child: FocusTraversalGroup(
///           child: Form(
89
///             autovalidateMode: AutovalidateMode.always,
90
///             onChanged: () {
91
///               Form.of(primaryFocus!.context!)!.save();
92 93 94 95 96 97
///             },
///             child: Wrap(
///               children: List<Widget>.generate(5, (int index) {
///                 return Padding(
///                   padding: const EdgeInsets.all(8.0),
///                   child: ConstrainedBox(
98
///                     constraints: BoxConstraints.tight(const Size(200, 50)),
99
///                     child: TextFormField(
100
///                       onSaved: (String? value) {
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
///                         print('Value for field $index saved as "$value"');
///                       },
///                     ),
///                   ),
///                 );
///               }),
///             ),
///           ),
///         ),
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
117 118
/// See also:
///
119
///  * <https://material.io/design/components/text-fields.html>
120 121 122 123
///  * [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.
124
///  * 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).
125
class TextFormField extends FormField<String> {
126 127
  /// Creates a [FormField] that contains a [TextField].
  ///
128 129 130
  /// 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
131
  /// to [initialValue] or the empty string.
132
  ///
133 134
  /// For documentation about the various parameters, see the [TextField] class
  /// and [new TextField], the constructor.
135
  TextFormField({
136
    Key? key,
137
    this.controller,
138 139 140 141
    String? initialValue,
    FocusNode? focusNode,
    InputDecoration? decoration = const InputDecoration(),
    TextInputType? keyboardType,
142
    TextCapitalization textCapitalization = TextCapitalization.none,
143 144 145 146
    TextInputAction? textInputAction,
    TextStyle? style,
    StrutStyle? strutStyle,
    TextDirection? textDirection,
147
    TextAlign textAlign = TextAlign.start,
148
    TextAlignVertical? textAlignVertical,
149
    bool autofocus = false,
150
    bool readOnly = false,
151 152
    ToolbarOptions? toolbarOptions,
    bool? showCursor,
153
    String obscuringCharacter = '•',
154 155
    bool obscureText = false,
    bool autocorrect = true,
156 157
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
158
    bool enableSuggestions = true,
159
    @Deprecated(
160
      'Use autovalidateMode parameter which provide more specific '
161 162 163
      'behaviour related to auto validation. '
      'This feature was deprecated after v1.19.0.'
    )
164
    bool autovalidate = false,
165 166 167 168 169
    @Deprecated(
      'Use maxLengthEnforcement parameter which provides more specific '
      'behavior related to the maxLength limit. '
      'This feature was deprecated after v1.25.0-5.0.pre.'
    )
170
    bool maxLengthEnforced = true,
171
    MaxLengthEnforcement? maxLengthEnforcement,
172 173
    int? maxLines = 1,
    int? minLines,
174
    bool expands = false,
175 176 177 178 179 180 181 182 183
    int? maxLength,
    ValueChanged<String>? onChanged,
    GestureTapCallback? onTap,
    VoidCallback? onEditingComplete,
    ValueChanged<String>? onFieldSubmitted,
    FormFieldSetter<String>? onSaved,
    FormFieldValidator<String>? validator,
    List<TextInputFormatter>? inputFormatters,
    bool? enabled,
184
    double cursorWidth = 2.0,
185 186 187 188
    double? cursorHeight,
    Radius? cursorRadius,
    Color? cursorColor,
    Brightness? keyboardAppearance,
189
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
190
    bool enableInteractiveSelection = true,
191
    TextSelectionControls? selectionControls,
192 193 194 195
    InputCounterWidgetBuilder? buildCounter,
    ScrollPhysics? scrollPhysics,
    Iterable<String>? autofillHints,
    AutovalidateMode? autovalidateMode,
196
    ScrollController? scrollController,
197
  }) : assert(initialValue == null || controller == null),
198
       assert(textAlign != null),
199
       assert(autofocus != null),
200
       assert(readOnly != null),
201
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
202
       assert(obscureText != null),
203
       assert(autocorrect != null),
204
       assert(enableSuggestions != null),
205
       assert(autovalidate != null),
206 207 208 209 210
       assert(
         autovalidate == false ||
         autovalidate == true && autovalidateMode == null,
         'autovalidate and autovalidateMode should not be used together.'
       ),
211
       assert(maxLengthEnforced != null),
212 213 214 215
       assert(
         maxLengthEnforced || maxLengthEnforcement == null,
         'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
       ),
216
       assert(scrollPadding != null),
217
       assert(maxLines == null || maxLines > 0),
218 219 220
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
221
         "minLines can't be greater than maxLines",
222 223 224 225 226 227
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
228
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
229
       assert(maxLength == null || maxLength > 0),
230
       assert(enableInteractiveSelection != null),
231
       super(
232 233 234 235 236 237 238 239 240 241 242
       key: key,
       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())
243
             .applyDefaults(Theme.of(field.context).inputDecorationTheme);
244
         void onChangedHandler(String value) {
245
           field.didChange(value);
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
           if (onChanged != null) {
             onChanged(value);
           }
         }
         return TextField(
           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,
273
           maxLengthEnforcement: maxLengthEnforcement,
274 275 276 277 278 279 280 281 282 283 284
           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,
285
           cursorHeight: cursorHeight,
286 287 288 289 290 291
           cursorRadius: cursorRadius,
           cursorColor: cursorColor,
           scrollPadding: scrollPadding,
           scrollPhysics: scrollPhysics,
           keyboardAppearance: keyboardAppearance,
           enableInteractiveSelection: enableInteractiveSelection,
292
           selectionControls: selectionControls,
293 294
           buildCounter: buildCounter,
           autofillHints: autofillHints,
295
           scrollController: scrollController,
296 297 298
         );
       },
     );
299 300 301

  /// Controls the text being edited.
  ///
302 303
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
304
  final TextEditingController? controller;
305 306

  @override
307
  _TextFormFieldState createState() => _TextFormFieldState();
308 309 310
}

class _TextFormFieldState extends FormFieldState<String> {
311
  TextEditingController? _controller;
312

313
  TextEditingController? get _effectiveController => widget.controller ?? _controller;
314 315

  @override
316
  TextFormField get widget => super.widget as TextFormField;
317 318 319 320 321

  @override
  void initState() {
    super.initState();
    if (widget.controller == null) {
322
      _controller = TextEditingController(text: widget.initialValue);
323
    } else {
324
      widget.controller!.addListener(_handleControllerChanged);
325 326 327 328 329 330 331 332 333 334 335
    }
  }

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

      if (oldWidget.controller != null && widget.controller == null)
336
        _controller = TextEditingController.fromValue(oldWidget.controller!.value);
337
      if (widget.controller != null) {
338
        setValue(widget.controller!.text);
339 340 341 342 343 344 345 346 347 348 349 350
        if (oldWidget.controller == null)
          _controller = null;
      }
    }
  }

  @override
  void dispose() {
    widget.controller?.removeListener(_handleControllerChanged);
    super.dispose();
  }

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

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

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