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

5 6
// @dart = 2.8

7 8 9 10 11
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import 'input_decorator.dart';
import 'text_field.dart';
12
import 'theme.dart';
13

14 15
export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;

16 17
/// A [FormField] that contains a [TextField].
///
18
/// This is a convenience widget that wraps a [TextField] widget in a
19 20 21 22 23 24 25
/// [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.
///
26 27 28 29 30 31 32 33 34
/// 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.
35
///
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 61 62 63 64 65 66 67 68 69
/// ```dart
/// TextFormField(
///   decoration: const InputDecoration(
///     icon: Icon(Icons.person),
///     hintText: 'What do people call you?',
///     labelText: 'Name *',
///   ),
///   onSaved: (String value) {
///     // This optional block of code can be used to run
///     // code when the user saves the form.
///   },
///   validator: (String value) {
///     return value.contains('@') ? 'Do not use the @ char.' : null;
///   },
/// )
/// ```
70
/// {@end-tool}
71
///
72 73 74 75 76 77 78 79 80 81 82 83 84 85
/// {@tool dartpad --template=stateful_widget_material}
/// This example shows how to move the focus to the next field when the user
/// presses the ENTER key.
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Material(
///     child: Center(
///       child: Shortcuts(
///         shortcuts: <LogicalKeySet, Intent>{
86 87
///           // Pressing space in the field will now move to the next field.
///           LogicalKeySet(LogicalKeyboardKey.space): const NextFocusIntent(),
88 89 90 91 92 93 94 95 96 97 98 99
///         },
///         child: FocusTraversalGroup(
///           child: Form(
///             autovalidate: true,
///             onChanged: () {
///               Form.of(primaryFocus.context).save();
///             },
///             child: Wrap(
///               children: List<Widget>.generate(5, (int index) {
///                 return Padding(
///                   padding: const EdgeInsets.all(8.0),
///                   child: ConstrainedBox(
100
///                     constraints: BoxConstraints.tight(const Size(200, 50)),
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
///                     child: TextFormField(
///                       onSaved: (String value) {
///                         print('Value for field $index saved as "$value"');
///                       },
///                     ),
///                   ),
///                 );
///               }),
///             ),
///           ),
///         ),
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
119 120
/// See also:
///
121
///  * <https://material.io/design/components/text-fields.html>
122 123 124 125
///  * [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.
126
///  * 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).
127
class TextFormField extends FormField<String> {
128 129
  /// Creates a [FormField] that contains a [TextField].
  ///
130 131 132
  /// 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
133
  /// to [initialValue] or the empty string.
134
  ///
135 136
  /// For documentation about the various parameters, see the [TextField] class
  /// and [new TextField], the constructor.
137 138
  TextFormField({
    Key key,
139
    this.controller,
140
    String initialValue,
141
    FocusNode focusNode,
142
    InputDecoration decoration = const InputDecoration(),
143
    TextInputType keyboardType,
144
    TextCapitalization textCapitalization = TextCapitalization.none,
145
    TextInputAction textInputAction,
146
    TextStyle style,
147
    StrutStyle strutStyle,
148
    TextDirection textDirection,
149
    TextAlign textAlign = TextAlign.start,
150
    TextAlignVertical textAlignVertical,
151
    bool autofocus = false,
152
    bool readOnly = false,
153
    ToolbarOptions toolbarOptions,
154
    bool showCursor,
155
    String obscuringCharacter = '•',
156 157
    bool obscureText = false,
    bool autocorrect = true,
158 159
    SmartDashesType smartDashesType,
    SmartQuotesType smartQuotesType,
160
    bool enableSuggestions = true,
161 162 163
    bool autovalidate = false,
    bool maxLengthEnforced = true,
    int maxLines = 1,
164 165
    int minLines,
    bool expands = false,
166
    int maxLength,
167
    ValueChanged<String> onChanged,
168
    GestureTapCallback onTap,
169
    VoidCallback onEditingComplete,
170
    ValueChanged<String> onFieldSubmitted,
171 172
    FormFieldSetter<String> onSaved,
    FormFieldValidator<String> validator,
173
    List<TextInputFormatter> inputFormatters,
174
    bool enabled,
175
    double cursorWidth = 2.0,
176
    double cursorHeight,
177 178
    Radius cursorRadius,
    Color cursorColor,
179
    Brightness keyboardAppearance,
180
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
181
    bool enableInteractiveSelection = true,
182
    InputCounterWidgetBuilder buildCounter,
183
    ScrollPhysics scrollPhysics,
184
    Iterable<String> autofillHints,
185
    AutovalidateMode autovalidateMode,
186
  }) : assert(initialValue == null || controller == null),
187
       assert(textAlign != null),
188
       assert(autofocus != null),
189
       assert(readOnly != null),
190
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
191
       assert(obscureText != null),
192
       assert(autocorrect != null),
193
       assert(enableSuggestions != null),
194
       assert(autovalidate != null),
195 196 197 198 199
       assert(
         autovalidate == false ||
         autovalidate == true && autovalidateMode == null,
         'autovalidate and autovalidateMode should not be used together.'
       ),
200
       assert(maxLengthEnforced != null),
201
       assert(scrollPadding != null),
202
       assert(maxLines == null || maxLines > 0),
203 204 205
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
206
         "minLines can't be greater than maxLines",
207 208 209 210 211 212
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
213
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
214
       assert(maxLength == null || maxLength > 0),
215
       assert(enableInteractiveSelection != null),
216
       super(
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 263 264 265 266 267 268
       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())
             .applyDefaults(Theme.of(field.context).inputDecorationTheme);
         void onChangedHandler(String value) {
           if (onChanged != null) {
             onChanged(value);
           }
           field.didChange(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,
           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,
269
           cursorHeight: cursorHeight,
270 271 272 273 274 275 276 277 278 279 280
           cursorRadius: cursorRadius,
           cursorColor: cursorColor,
           scrollPadding: scrollPadding,
           scrollPhysics: scrollPhysics,
           keyboardAppearance: keyboardAppearance,
           enableInteractiveSelection: enableInteractiveSelection,
           buildCounter: buildCounter,
           autofillHints: autofillHints,
         );
       },
     );
281 282 283

  /// Controls the text being edited.
  ///
284 285
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
286 287 288
  final TextEditingController controller;

  @override
289
  _TextFormFieldState createState() => _TextFormFieldState();
290 291 292 293 294 295 296 297
}

class _TextFormFieldState extends FormFieldState<String> {
  TextEditingController _controller;

  TextEditingController get _effectiveController => widget.controller ?? _controller;

  @override
298
  TextFormField get widget => super.widget as TextFormField;
299 300 301 302 303

  @override
  void initState() {
    super.initState();
    if (widget.controller == null) {
304
      _controller = TextEditingController(text: widget.initialValue);
305 306 307 308 309 310 311 312 313 314 315 316 317
    } else {
      widget.controller.addListener(_handleControllerChanged);
    }
  }

  @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)
318
        _controller = TextEditingController.fromValue(oldWidget.controller.value);
319 320 321 322 323 324 325 326 327 328 329 330 331 332
      if (widget.controller != null) {
        setValue(widget.controller.text);
        if (oldWidget.controller == null)
          _controller = null;
      }
    }
  }

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

333 334 335 336 337 338 339 340
  @override
  void didChange(String value) {
    super.didChange(value);

    if (_effectiveController.text != value)
      _effectiveController.text = value;
  }

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  @override
  void reset() {
    super.reset();
    setState(() {
      _effectiveController.text = widget.initialValue;
    });
  }

  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.
    if (_effectiveController.text != value)
358
      didChange(_effectiveController.text);
359
  }
360
}