text_form_field_row.dart 13 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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 'colors.dart';
import 'form_row.dart';
import 'text_field.dart';

/// Creates a [CupertinoFormRow] containing a [FormField] that wraps
/// a [CupertinoTextField].
///
/// 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.
///
/// 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.
///
/// The [prefix] parameter is displayed at the start of the row. Standard iOS
/// guidelines encourage passing a [Text] widget to [prefix] to detail the
/// nature of the input.
///
/// The [padding] parameter is used to pad the contents of the row. It is
/// directly passed to [CupertinoFormRow]. If the [padding]
/// parameter is null, [CupertinoFormRow] constructs its own default
/// padding (which is the standard form row padding in iOS.) If no edge
/// insets are intended, explicitly pass [EdgeInsets.zero] to [padding].
///
/// If a [controller] is not specified, [initialValue] can be used to give
/// the automatically generated controller an initial value.
///
/// Consider calling [TextEditingController.dispose] of the [controller], if one
/// is specified, when it is no longer needed. This will ensure we discard any
/// resources used by the object.
///
/// For documentation about the various parameters, see the
45
/// [CupertinoTextField] class and [CupertinoTextField.borderless],
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
/// the constructor.
///
/// {@tool snippet}
///
/// Creates a [CupertinoTextFormFieldRow] with a leading text and validator
/// function.
///
/// If the user enters valid text, the CupertinoTextField appears normally
/// without any warnings to the user.
///
/// If the user enters invalid text, the error message returned from the
/// validator function is displayed in dark red underneath the input.
///
/// ```dart
/// CupertinoTextFormFieldRow(
61
///   prefix: const Text('Username'),
62
///   onSaved: (String? value) {
63 64 65
///     // This optional block of code can be used to run
///     // code when the user saves the form.
///   },
66 67
///   validator: (String? value) {
///     return (value != null && value.contains('@')) ? 'Do not use the @ char.' : null;
68 69 70 71 72
///   },
/// )
/// ```
/// {@end-tool}
///
73
/// {@tool dartpad}
74 75 76
/// This example shows how to move the focus to the next field when the user
/// presses the SPACE key.
///
77
/// ** See code in examples/api/lib/cupertino/text_form_field_row/cupertino_text_form_field_row.1.dart **
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
/// {@end-tool}
class CupertinoTextFormFieldRow extends FormField<String> {
  /// Creates a [CupertinoFormRow] containing a [FormField] that wraps
  /// a [CupertinoTextField].
  ///
  /// 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
  /// to [initialValue] or the empty string.
  ///
  /// The [prefix] parameter is displayed at the start of the row. Standard iOS
  /// guidelines encourage passing a [Text] widget to [prefix] to detail the
  /// nature of the input.
  ///
  /// The [padding] parameter is used to pad the contents of the row. It is
  /// directly passed to [CupertinoFormRow]. If the [padding]
  /// parameter is null, [CupertinoFormRow] constructs its own default
  /// padding (which is the standard form row padding in iOS.) If no edge
  /// insets are intended, explicitly pass [EdgeInsets.zero] to [padding].
  ///
  /// For documentation about the various parameters, see the
99
  /// [CupertinoTextField] class and [CupertinoTextField.borderless],
100 101 102 103 104 105 106 107 108 109 110 111 112 113
  /// the constructor.
  CupertinoTextFormFieldRow({
    Key? key,
    this.prefix,
    this.padding,
    this.controller,
    String? initialValue,
    FocusNode? focusNode,
    BoxDecoration? decoration,
    TextInputType? keyboardType,
    TextCapitalization textCapitalization = TextCapitalization.none,
    TextInputAction? textInputAction,
    TextStyle? style,
    StrutStyle? strutStyle,
114
    TextDirection? textDirection,
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
    TextAlign textAlign = TextAlign.start,
    TextAlignVertical? textAlignVertical,
    bool autofocus = false,
    bool readOnly = false,
    ToolbarOptions? toolbarOptions,
    bool? showCursor,
    String obscuringCharacter = '•',
    bool obscureText = false,
    bool autocorrect = true,
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
    bool enableSuggestions = true,
    int? maxLines = 1,
    int? minLines,
    bool expands = false,
    int? maxLength,
    ValueChanged<String>? onChanged,
    GestureTapCallback? onTap,
    VoidCallback? onEditingComplete,
    ValueChanged<String>? onFieldSubmitted,
    FormFieldSetter<String>? onSaved,
    FormFieldValidator<String>? validator,
    List<TextInputFormatter>? inputFormatters,
    bool? enabled,
    double cursorWidth = 2.0,
    double? cursorHeight,
    Color? cursorColor,
    Brightness? keyboardAppearance,
    EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
    bool enableInteractiveSelection = true,
    TextSelectionControls? selectionControls,
    ScrollPhysics? scrollPhysics,
    Iterable<String>? autofillHints,
    AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
    String? placeholder,
    TextStyle? placeholderStyle = const TextStyle(
      fontWeight: FontWeight.w400,
      color: CupertinoColors.placeholderText,
    ),
  })  : assert(initialValue == null || controller == null),
        assert(textAlign != null),
        assert(autofocus != null),
        assert(readOnly != null),
        assert(obscuringCharacter != null && obscuringCharacter.length == 1),
        assert(obscureText != null),
        assert(autocorrect != null),
        assert(enableSuggestions != null),
        assert(scrollPadding != null),
        assert(maxLines == null || maxLines > 0),
        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.',
        ),
174
        assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
        assert(maxLength == null || maxLength > 0),
        assert(enableInteractiveSelection != null),
        super(
          key: key,
          initialValue: controller?.text ?? initialValue ?? '',
          onSaved: onSaved,
          validator: validator,
          autovalidateMode: autovalidateMode,
          builder: (FormFieldState<String> field) {
            final _CupertinoTextFormFieldRowState state =
                field as _CupertinoTextFormFieldRowState;

            void onChangedHandler(String value) {
              field.didChange(value);
              if (onChanged != null) {
                onChanged(value);
              }
            }

            return CupertinoFormRow(
              prefix: prefix,
              padding: padding,
              error: (field.errorText == null) ? null : Text(field.errorText!),
              child: CupertinoTextField.borderless(
                controller: state._effectiveController,
                focusNode: focusNode,
                keyboardType: keyboardType,
                decoration: decoration,
                textInputAction: textInputAction,
                style: style,
                strutStyle: strutStyle,
                textAlign: textAlign,
                textAlignVertical: textAlignVertical,
                textCapitalization: textCapitalization,
209
                textDirection: textDirection,
210 211 212 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 263 264 265 266 267 268 269
                autofocus: autofocus,
                toolbarOptions: toolbarOptions,
                readOnly: readOnly,
                showCursor: showCursor,
                obscuringCharacter: obscuringCharacter,
                obscureText: obscureText,
                autocorrect: autocorrect,
                smartDashesType: smartDashesType,
                smartQuotesType: smartQuotesType,
                enableSuggestions: enableSuggestions,
                maxLines: maxLines,
                minLines: minLines,
                expands: expands,
                maxLength: maxLength,
                onChanged: onChangedHandler,
                onTap: onTap,
                onEditingComplete: onEditingComplete,
                onSubmitted: onFieldSubmitted,
                inputFormatters: inputFormatters,
                enabled: enabled,
                cursorWidth: cursorWidth,
                cursorHeight: cursorHeight,
                cursorColor: cursorColor,
                scrollPadding: scrollPadding,
                scrollPhysics: scrollPhysics,
                keyboardAppearance: keyboardAppearance,
                enableInteractiveSelection: enableInteractiveSelection,
                selectionControls: selectionControls,
                autofillHints: autofillHints,
                placeholder: placeholder,
                placeholderStyle: placeholderStyle,
              ),
            );
          },
        );

  /// A widget that is displayed at the start of the row.
  ///
  /// The [prefix] widget is displayed at the start of the row. Standard iOS
  /// guidelines encourage passing a [Text] widget to [prefix] to detail the
  /// nature of the input.
  final Widget? prefix;

  /// Content padding for the row.
  ///
  /// The [padding] widget is passed to [CupertinoFormRow]. If the [padding]
  /// parameter is null, [CupertinoFormRow] constructs its own default
  /// padding, which is the standard form row padding in iOS.
  ///
  /// If no edge insets are intended, explicitly pass [EdgeInsets.zero] to
  /// [padding].
  final EdgeInsetsGeometry? padding;

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

  @override
270
  FormFieldState<String> createState() => _CupertinoTextFormFieldRowState();
271 272 273 274 275 276
}

class _CupertinoTextFormFieldRowState extends FormFieldState<String> {
  TextEditingController? _controller;

  TextEditingController? get _effectiveController =>
277
      _cupertinoTextFormFieldRow.controller ?? _controller;
278

279
  CupertinoTextFormFieldRow get _cupertinoTextFormFieldRow =>
280 281 282 283 284
      super.widget as CupertinoTextFormFieldRow;

  @override
  void initState() {
    super.initState();
285
    if (_cupertinoTextFormFieldRow.controller == null) {
286 287
      _controller = TextEditingController(text: widget.initialValue);
    } else {
288
      _cupertinoTextFormFieldRow.controller!.addListener(_handleControllerChanged);
289 290 291 292 293 294
    }
  }

  @override
  void didUpdateWidget(CupertinoTextFormFieldRow oldWidget) {
    super.didUpdateWidget(oldWidget);
295
    if (_cupertinoTextFormFieldRow.controller != oldWidget.controller) {
296
      oldWidget.controller?.removeListener(_handleControllerChanged);
297
      _cupertinoTextFormFieldRow.controller?.addListener(_handleControllerChanged);
298

299
      if (oldWidget.controller != null && _cupertinoTextFormFieldRow.controller == null) {
300 301 302 303
        _controller =
            TextEditingController.fromValue(oldWidget.controller!.value);
      }

304 305
      if (_cupertinoTextFormFieldRow.controller != null) {
        setValue(_cupertinoTextFormFieldRow.controller!.text);
306 307 308 309 310 311 312 313 314
        if (oldWidget.controller == null) {
          _controller = null;
        }
      }
    }
  }

  @override
  void dispose() {
315
    _cupertinoTextFormFieldRow.controller?.removeListener(_handleControllerChanged);
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
    super.dispose();
  }

  @override
  void didChange(String? value) {
    super.didChange(value);

    if (value != null && _effectiveController!.text != value) {
      _effectiveController!.text = value;
    }
  }

  @override
  void reset() {
    super.reset();

    if (widget.initialValue != null) {
      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) {
      didChange(_effectiveController!.text);
    }
  }
}