text_form_field.dart 10.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 [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
///
/// See also:
///
67
///  * <https://material.io/design/components/text-fields.html>
68 69 70 71
///  * [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.
72
///  * 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)
73
class TextFormField extends FormField<String> {
74 75
  /// Creates a [FormField] that contains a [TextField].
  ///
76 77 78
  /// 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
79
  /// to [initialValue] or the empty string.
80
  ///
81 82
  /// For documentation about the various parameters, see the [TextField] class
  /// and [new TextField], the constructor.
83 84
  TextFormField({
    Key key,
85
    this.controller,
86
    String initialValue,
87
    FocusNode focusNode,
88
    InputDecoration decoration = const InputDecoration(),
89
    TextInputType keyboardType,
90
    TextCapitalization textCapitalization = TextCapitalization.none,
91
    TextInputAction textInputAction,
92
    TextStyle style,
93
    StrutStyle strutStyle,
94
    TextDirection textDirection,
95
    TextAlign textAlign = TextAlign.start,
96
    TextAlignVertical textAlignVertical,
97
    bool autofocus = false,
98
    bool readOnly = false,
99
    ToolbarOptions toolbarOptions,
100
    bool showCursor,
101 102
    bool obscureText = false,
    bool autocorrect = true,
103 104
    SmartDashesType smartDashesType,
    SmartQuotesType smartQuotesType,
105
    bool enableSuggestions = true,
106 107 108
    bool autovalidate = false,
    bool maxLengthEnforced = true,
    int maxLines = 1,
109 110
    int minLines,
    bool expands = false,
111
    int maxLength,
112
    ValueChanged<String> onChanged,
113
    GestureTapCallback onTap,
114
    VoidCallback onEditingComplete,
115
    ValueChanged<String> onFieldSubmitted,
116 117
    FormFieldSetter<String> onSaved,
    FormFieldValidator<String> validator,
118
    List<TextInputFormatter> inputFormatters,
119
    bool enabled = true,
120 121 122
    double cursorWidth = 2.0,
    Radius cursorRadius,
    Color cursorColor,
123
    Brightness keyboardAppearance,
124
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
125
    bool enableInteractiveSelection = true,
126
    InputCounterWidgetBuilder buildCounter,
127
    ScrollPhysics scrollPhysics,
128
  }) : assert(initialValue == null || controller == null),
129
       assert(textAlign != null),
130
       assert(autofocus != null),
131
       assert(readOnly != null),
132
       assert(obscureText != null),
133
       assert(autocorrect != null),
134
       assert(enableSuggestions != null),
135
       assert(autovalidate != null),
136
       assert(maxLengthEnforced != null),
137
       assert(scrollPadding != null),
138
       assert(maxLines == null || maxLines > 0),
139 140 141 142 143 144 145 146 147 148
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
         'minLines can\'t be greater than maxLines',
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
149
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
150
       assert(maxLength == null || maxLength > 0),
151
       assert(enableInteractiveSelection != null),
152
       super(
153
    key: key,
154
    initialValue: controller != null ? controller.text : (initialValue ?? ''),
155 156
    onSaved: onSaved,
    validator: validator,
157
    autovalidate: autovalidate,
158
    enabled: enabled,
159
    builder: (FormFieldState<String> field) {
160
      final _TextFormFieldState state = field as _TextFormFieldState;
161 162
      final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
        .applyDefaults(Theme.of(field.context).inputDecorationTheme);
163 164 165 166 167 168
      void onChangedHandler(String value) {
        if (onChanged != null) {
          onChanged(value);
        }
        field.didChange(value);
      }
169
      return TextField(
170
        controller: state._effectiveController,
171
        focusNode: focusNode,
172
        decoration: effectiveDecoration.copyWith(errorText: field.errorText),
173
        keyboardType: keyboardType,
174
        textInputAction: textInputAction,
175
        style: style,
176
        strutStyle: strutStyle,
177
        textAlign: textAlign,
178
        textAlignVertical: textAlignVertical,
179
        textDirection: textDirection,
180
        textCapitalization: textCapitalization,
181
        autofocus: autofocus,
182
        toolbarOptions: toolbarOptions,
183 184
        readOnly: readOnly,
        showCursor: showCursor,
185
        obscureText: obscureText,
186
        autocorrect: autocorrect,
187 188
        smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
        smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
189
        enableSuggestions: enableSuggestions,
190
        maxLengthEnforced: maxLengthEnforced,
191
        maxLines: maxLines,
192 193
        minLines: minLines,
        expands: expands,
194
        maxLength: maxLength,
195
        onChanged: onChangedHandler,
196
        onTap: onTap,
197
        onEditingComplete: onEditingComplete,
198
        onSubmitted: onFieldSubmitted,
199
        inputFormatters: inputFormatters,
200
        enabled: enabled,
201 202 203
        cursorWidth: cursorWidth,
        cursorRadius: cursorRadius,
        cursorColor: cursorColor,
204
        scrollPadding: scrollPadding,
205
        scrollPhysics: scrollPhysics,
206
        keyboardAppearance: keyboardAppearance,
207
        enableInteractiveSelection: enableInteractiveSelection,
208
        buildCounter: buildCounter,
209 210 211
      );
    },
  );
212 213 214

  /// Controls the text being edited.
  ///
215 216
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
217 218 219
  final TextEditingController controller;

  @override
220
  _TextFormFieldState createState() => _TextFormFieldState();
221 222 223 224 225 226 227 228
}

class _TextFormFieldState extends FormFieldState<String> {
  TextEditingController _controller;

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

  @override
229
  TextFormField get widget => super.widget as TextFormField;
230 231 232 233 234

  @override
  void initState() {
    super.initState();
    if (widget.controller == null) {
235
      _controller = TextEditingController(text: widget.initialValue);
236 237 238 239 240 241 242 243 244 245 246 247 248
    } 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)
249
        _controller = TextEditingController.fromValue(oldWidget.controller.value);
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
      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)
281
      didChange(_effectiveController.text);
282
  }
283
}