text_form_field.dart 11.9 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 [dispose] of the [TextEditingController] when it is no longer needed.
/// This will ensure we discard any resources used by the object.
///
37 38
/// For a documentation about the various parameters, see [TextField].
///
39
/// {@tool snippet}
40 41 42
///
/// Creates a [TextFormField] with an [InputDecoration] and validator function.
///
43 44 45 46
/// ![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)
///
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
/// ```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;
///   },
/// )
/// ```
63
/// {@end-tool}
64
///
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
/// {@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>{
///           // Pressing enter on the field will now move to the next field.
///           LogicalKeySet(LogicalKeyboardKey.enter):
///               Intent(NextFocusAction.key),
///         },
///         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(
///                     constraints: BoxConstraints.tight(Size(200, 50)),
///                     child: TextFormField(
///                       onSaved: (String value) {
///                         print('Value for field $index saved as "$value"');
///                       },
///                     ),
///                   ),
///                 );
///               }),
///             ),
///           ),
///         ),
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
113 114
/// See also:
///
115
///  * <https://material.io/design/components/text-fields.html>
116 117 118 119
///  * [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.
120
///  * Learn how to use a [TextEditingController] in one of our [cookbook recipe]s.(https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller)
121
class TextFormField extends FormField<String> {
122 123
  /// Creates a [FormField] that contains a [TextField].
  ///
124 125 126
  /// 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
127
  /// to [initialValue] or the empty string.
128
  ///
129 130
  /// For documentation about the various parameters, see the [TextField] class
  /// and [new TextField], the constructor.
131 132
  TextFormField({
    Key key,
133
    this.controller,
134
    String initialValue,
135
    FocusNode focusNode,
136
    InputDecoration decoration = const InputDecoration(),
137
    TextInputType keyboardType,
138
    TextCapitalization textCapitalization = TextCapitalization.none,
139
    TextInputAction textInputAction,
140
    TextStyle style,
141
    StrutStyle strutStyle,
142
    TextDirection textDirection,
143
    TextAlign textAlign = TextAlign.start,
144
    TextAlignVertical textAlignVertical,
145
    bool autofocus = false,
146
    bool readOnly = false,
147
    ToolbarOptions toolbarOptions,
148
    bool showCursor,
149 150
    bool obscureText = false,
    bool autocorrect = true,
151 152
    SmartDashesType smartDashesType,
    SmartQuotesType smartQuotesType,
153
    bool enableSuggestions = true,
154 155 156
    bool autovalidate = false,
    bool maxLengthEnforced = true,
    int maxLines = 1,
157 158
    int minLines,
    bool expands = false,
159
    int maxLength,
160
    ValueChanged<String> onChanged,
161
    GestureTapCallback onTap,
162
    VoidCallback onEditingComplete,
163
    ValueChanged<String> onFieldSubmitted,
164 165
    FormFieldSetter<String> onSaved,
    FormFieldValidator<String> validator,
166
    List<TextInputFormatter> inputFormatters,
167
    bool enabled = true,
168 169 170
    double cursorWidth = 2.0,
    Radius cursorRadius,
    Color cursorColor,
171
    Brightness keyboardAppearance,
172
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
173
    bool enableInteractiveSelection = true,
174
    InputCounterWidgetBuilder buildCounter,
175
    ScrollPhysics scrollPhysics,
176
  }) : assert(initialValue == null || controller == null),
177
       assert(textAlign != null),
178
       assert(autofocus != null),
179
       assert(readOnly != null),
180
       assert(obscureText != null),
181
       assert(autocorrect != null),
182
       assert(enableSuggestions != null),
183
       assert(autovalidate != null),
184
       assert(maxLengthEnforced != null),
185
       assert(scrollPadding != null),
186
       assert(maxLines == null || maxLines > 0),
187 188 189
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
190
         "minLines can't be greater than maxLines",
191 192 193 194 195 196
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
197
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
198
       assert(maxLength == null || maxLength > 0),
199
       assert(enableInteractiveSelection != null),
200
       super(
201
    key: key,
202
    initialValue: controller != null ? controller.text : (initialValue ?? ''),
203 204
    onSaved: onSaved,
    validator: validator,
205
    autovalidate: autovalidate,
206
    enabled: enabled,
207
    builder: (FormFieldState<String> field) {
208
      final _TextFormFieldState state = field as _TextFormFieldState;
209 210
      final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
        .applyDefaults(Theme.of(field.context).inputDecorationTheme);
211 212 213 214 215 216
      void onChangedHandler(String value) {
        if (onChanged != null) {
          onChanged(value);
        }
        field.didChange(value);
      }
217
      return TextField(
218
        controller: state._effectiveController,
219
        focusNode: focusNode,
220
        decoration: effectiveDecoration.copyWith(errorText: field.errorText),
221
        keyboardType: keyboardType,
222
        textInputAction: textInputAction,
223
        style: style,
224
        strutStyle: strutStyle,
225
        textAlign: textAlign,
226
        textAlignVertical: textAlignVertical,
227
        textDirection: textDirection,
228
        textCapitalization: textCapitalization,
229
        autofocus: autofocus,
230
        toolbarOptions: toolbarOptions,
231 232
        readOnly: readOnly,
        showCursor: showCursor,
233
        obscureText: obscureText,
234
        autocorrect: autocorrect,
235 236
        smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
        smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
237
        enableSuggestions: enableSuggestions,
238
        maxLengthEnforced: maxLengthEnforced,
239
        maxLines: maxLines,
240 241
        minLines: minLines,
        expands: expands,
242
        maxLength: maxLength,
243
        onChanged: onChangedHandler,
244
        onTap: onTap,
245
        onEditingComplete: onEditingComplete,
246
        onSubmitted: onFieldSubmitted,
247
        inputFormatters: inputFormatters,
248
        enabled: enabled,
249 250 251
        cursorWidth: cursorWidth,
        cursorRadius: cursorRadius,
        cursorColor: cursorColor,
252
        scrollPadding: scrollPadding,
253
        scrollPhysics: scrollPhysics,
254
        keyboardAppearance: keyboardAppearance,
255
        enableInteractiveSelection: enableInteractiveSelection,
256
        buildCounter: buildCounter,
257 258 259
      );
    },
  );
260 261 262

  /// Controls the text being edited.
  ///
263 264
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
265 266 267
  final TextEditingController controller;

  @override
268
  _TextFormFieldState createState() => _TextFormFieldState();
269 270 271 272 273 274 275 276
}

class _TextFormFieldState extends FormFieldState<String> {
  TextEditingController _controller;

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

  @override
277
  TextFormField get widget => super.widget as TextFormField;
278 279 280 281 282

  @override
  void initState() {
    super.initState();
    if (widget.controller == null) {
283
      _controller = TextEditingController(text: widget.initialValue);
284 285 286 287 288 289 290 291 292 293 294 295 296
    } 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)
297
        _controller = TextEditingController.fromValue(oldWidget.controller.value);
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
      if (widget.controller != null) {
        setValue(widget.controller.text);
        if (oldWidget.controller == null)
          _controller = null;
      }
    }
  }

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

  @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)
329
      didChange(_effectiveController.text);
330
  }
331
}