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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
113
114
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
174
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
209
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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// 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
/// [CupertinoTextField] class and [new CupertinoTextField.borderless],
/// 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(
/// prefix: const Text('Username'),
/// 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 != null && value.contains('@')) ? 'Do not use the @ char.' : null;
/// },
/// )
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateful_widget_material}
/// This example shows how to move the focus to the next field when the user
/// presses the SPACE key.
///
/// ```dart imports
/// import 'package:flutter/cupertino.dart';
/// ```
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// child: Center(
/// child: Form(
/// autovalidateMode: AutovalidateMode.always,
/// onChanged: () {
/// Form.of(primaryFocus!.context!)?.save();
/// },
/// child: CupertinoFormSection.insetGrouped(
/// header: const Text('SECTION 1'),
/// children: List<Widget>.generate(5, (int index) {
/// return CupertinoTextFormFieldRow(
/// prefix: const Text('Enter text'),
/// placeholder: 'Enter text',
/// validator: (String? value) {
/// if (value == null || value.isEmpty) {
/// return 'Please enter a value';
/// }
/// return null;
/// },
/// );
/// }),
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@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
/// [CupertinoTextField] class and [new CupertinoTextField.borderless],
/// 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,
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.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
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,
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
_CupertinoTextFormFieldRowState createState() =>
_CupertinoTextFormFieldRowState();
}
class _CupertinoTextFormFieldRowState extends FormFieldState<String> {
TextEditingController? _controller;
TextEditingController? get _effectiveController =>
widget.controller ?? _controller;
@override
CupertinoTextFormFieldRow get widget =>
super.widget as CupertinoTextFormFieldRow;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = TextEditingController(text: widget.initialValue);
} else {
widget.controller!.addListener(_handleControllerChanged);
}
}
@override
void didUpdateWidget(CupertinoTextFormFieldRow oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null) {
_controller =
TextEditingController.fromValue(oldWidget.controller!.value);
}
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 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);
}
}
}