FormField should autovalidate only if its content was changed (fixed) (#59766)
This commit is contained in:
parent
9c4a5ef1ed
commit
5a69de8263
@ -1437,7 +1437,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
/// Creates a [DropdownButton] widget that is a [FormField], wrapped in an
|
||||
/// [InputDecorator].
|
||||
///
|
||||
/// For a description of the `onSaved`, `validator`, or `autovalidate`
|
||||
/// For a description of the `onSaved`, `validator`, or `autovalidateMode`
|
||||
/// parameters, see [FormField]. For the rest (other than [decoration]), see
|
||||
/// [DropdownButton].
|
||||
///
|
||||
@ -1469,6 +1469,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
FormFieldSetter<T> onSaved,
|
||||
FormFieldValidator<T> validator,
|
||||
bool autovalidate = false,
|
||||
AutovalidateMode autovalidateMode,
|
||||
}) : assert(items == null || items.isEmpty || value == null ||
|
||||
items.where((DropdownMenuItem<T> item) {
|
||||
return item.value == value;
|
||||
@ -1484,13 +1485,21 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
assert(isExpanded != null),
|
||||
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
|
||||
assert(autofocus != null),
|
||||
assert(autovalidate != null),
|
||||
assert(
|
||||
autovalidate == false ||
|
||||
autovalidate == true && autovalidateMode == null,
|
||||
'autovalidate and autovalidateMode should not be used together.'
|
||||
),
|
||||
decoration = decoration ?? InputDecoration(focusColor: focusColor),
|
||||
super(
|
||||
key: key,
|
||||
onSaved: onSaved,
|
||||
initialValue: value,
|
||||
validator: validator,
|
||||
autovalidate: autovalidate,
|
||||
autovalidateMode: autovalidate
|
||||
? AutovalidateMode.always
|
||||
: (autovalidateMode ?? AutovalidateMode.disabled),
|
||||
builder: (FormFieldState<T> field) {
|
||||
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
|
||||
final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor);
|
||||
|
@ -180,6 +180,7 @@ class TextFormField extends FormField<String> {
|
||||
InputCounterWidgetBuilder buildCounter,
|
||||
ScrollPhysics scrollPhysics,
|
||||
Iterable<String> autofillHints,
|
||||
AutovalidateMode autovalidateMode,
|
||||
}) : assert(initialValue == null || controller == null),
|
||||
assert(textAlign != null),
|
||||
assert(autofocus != null),
|
||||
@ -189,6 +190,11 @@ class TextFormField extends FormField<String> {
|
||||
assert(autocorrect != null),
|
||||
assert(enableSuggestions != null),
|
||||
assert(autovalidate != null),
|
||||
assert(
|
||||
autovalidate == false ||
|
||||
autovalidate == true && autovalidateMode == null,
|
||||
'autovalidate and autovalidateMode should not be used together.'
|
||||
),
|
||||
assert(maxLengthEnforced != null),
|
||||
assert(scrollPadding != null),
|
||||
assert(maxLines == null || maxLines > 0),
|
||||
@ -206,67 +212,69 @@ class TextFormField extends FormField<String> {
|
||||
assert(maxLength == null || maxLength > 0),
|
||||
assert(enableInteractiveSelection != null),
|
||||
super(
|
||||
key: key,
|
||||
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
autovalidate: autovalidate,
|
||||
enabled: enabled ?? decoration?.enabled ?? true,
|
||||
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,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
scrollPadding: scrollPadding,
|
||||
scrollPhysics: scrollPhysics,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
buildCounter: buildCounter,
|
||||
autofillHints: autofillHints,
|
||||
);
|
||||
},
|
||||
);
|
||||
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,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
scrollPadding: scrollPadding,
|
||||
scrollPhysics: scrollPhysics,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
buildCounter: buildCounter,
|
||||
autofillHints: autofillHints,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
|
@ -81,7 +81,17 @@ class Form extends StatefulWidget {
|
||||
this.autovalidate = false,
|
||||
this.onWillPop,
|
||||
this.onChanged,
|
||||
AutovalidateMode autovalidateMode,
|
||||
}) : assert(child != null),
|
||||
assert(autovalidate != null),
|
||||
assert(
|
||||
autovalidate == false ||
|
||||
autovalidate == true && autovalidateMode == null,
|
||||
'autovalidate and autovalidateMode should not be used together.'
|
||||
),
|
||||
autovalidateMode = autovalidate
|
||||
? AutovalidateMode.always
|
||||
: (autovalidateMode ?? AutovalidateMode.disabled),
|
||||
super(key: key);
|
||||
|
||||
/// Returns the closest [FormState] which encloses the given context.
|
||||
@ -127,6 +137,12 @@ class Form extends StatefulWidget {
|
||||
/// will rebuild.
|
||||
final VoidCallback onChanged;
|
||||
|
||||
/// Used to enable/disable form fields auto validation and update their error
|
||||
/// text.
|
||||
///
|
||||
/// {@macro flutter.widgets.form.autovalidateMode}
|
||||
final AutovalidateMode autovalidateMode;
|
||||
|
||||
@override
|
||||
FormState createState() => FormState();
|
||||
}
|
||||
@ -139,6 +155,7 @@ class Form extends StatefulWidget {
|
||||
/// Typically obtained via [Form.of].
|
||||
class FormState extends State<Form> {
|
||||
int _generation = 0;
|
||||
bool _hasInteractedByUser = false;
|
||||
final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{};
|
||||
|
||||
// Called when a form field has changed. This will cause all form fields
|
||||
@ -146,6 +163,10 @@ class FormState extends State<Form> {
|
||||
void _fieldDidChange() {
|
||||
if (widget.onChanged != null)
|
||||
widget.onChanged();
|
||||
|
||||
|
||||
_hasInteractedByUser = _fields
|
||||
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser);
|
||||
_forceRebuild();
|
||||
}
|
||||
|
||||
@ -165,8 +186,19 @@ class FormState extends State<Form> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.autovalidate)
|
||||
_validate();
|
||||
switch (widget.autovalidateMode) {
|
||||
case AutovalidateMode.always:
|
||||
_validate();
|
||||
break;
|
||||
case AutovalidateMode.onUserInteraction:
|
||||
if (_hasInteractedByUser) {
|
||||
_validate();
|
||||
}
|
||||
break;
|
||||
case AutovalidateMode.disabled:
|
||||
break;
|
||||
}
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: widget.onWillPop,
|
||||
child: _FormScope(
|
||||
@ -188,11 +220,12 @@ class FormState extends State<Form> {
|
||||
///
|
||||
/// The [Form.onChanged] callback will be called.
|
||||
///
|
||||
/// If the form's [Form.autovalidate] property is true, the fields will all be
|
||||
/// revalidated after being reset.
|
||||
/// If the form's [Form.autovalidateMode] property is [AutovalidateMode.always],
|
||||
/// the fields will all be revalidated after being reset.
|
||||
void reset() {
|
||||
for (final FormFieldState<dynamic> field in _fields)
|
||||
field.reset();
|
||||
_hasInteractedByUser = false;
|
||||
_fieldDidChange();
|
||||
}
|
||||
|
||||
@ -201,6 +234,7 @@ class FormState extends State<Form> {
|
||||
///
|
||||
/// The form will rebuild to report the results.
|
||||
bool validate() {
|
||||
_hasInteractedByUser = true;
|
||||
_forceRebuild();
|
||||
return _validate();
|
||||
}
|
||||
@ -287,7 +321,16 @@ class FormField<T> extends StatefulWidget {
|
||||
this.initialValue,
|
||||
this.autovalidate = false,
|
||||
this.enabled = true,
|
||||
AutovalidateMode autovalidateMode,
|
||||
}) : assert(builder != null),
|
||||
assert(
|
||||
autovalidate == false ||
|
||||
autovalidate == true && autovalidateMode == null,
|
||||
'autovalidate and autovalidateMode should not be used together.'
|
||||
),
|
||||
autovalidateMode = autovalidate
|
||||
? AutovalidateMode.always
|
||||
: (autovalidateMode ?? AutovalidateMode.disabled),
|
||||
super(key: key);
|
||||
|
||||
/// An optional method to call with the final value when the form is saved via
|
||||
@ -325,11 +368,26 @@ class FormField<T> extends StatefulWidget {
|
||||
|
||||
/// Whether the form is able to receive user input.
|
||||
///
|
||||
/// Defaults to true. If [autovalidate] is true, the field will be validated.
|
||||
/// Likewise, if this field is false, the widget will not be validated
|
||||
/// regardless of [autovalidate].
|
||||
/// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled],
|
||||
/// the field will be auto validated. Likewise, if this field is false, the widget
|
||||
/// will not be validated regardless of [autovalidateMode].
|
||||
final bool enabled;
|
||||
|
||||
/// Used to enable/disable this form field auto validation and update its
|
||||
/// error text.
|
||||
///
|
||||
/// {@template flutter.widgets.form.autovalidateMode}
|
||||
/// If [AutovalidateMode.onUserInteraction] this form field will only
|
||||
/// auto-validate after its content changes, if [AutovalidateMode.always] it
|
||||
/// will auto validate even without user interaction and
|
||||
/// if [AutovalidateMode.disabled] the auto validation will be disabled.
|
||||
///
|
||||
/// Defaults to [AutovalidateMode.disabled] if [autovalidate] is false which
|
||||
/// means no auto validation will occur. If [autovalidate] is true then this
|
||||
/// is set to [AutovalidateMode.always] for backward compatibility.
|
||||
/// {@endtemplate}
|
||||
final AutovalidateMode autovalidateMode;
|
||||
|
||||
@override
|
||||
FormFieldState<T> createState() => FormFieldState<T>();
|
||||
}
|
||||
@ -339,6 +397,7 @@ class FormField<T> extends StatefulWidget {
|
||||
class FormFieldState<T> extends State<FormField<T>> {
|
||||
T _value;
|
||||
String _errorText;
|
||||
bool _hasInteractedByUser = false;
|
||||
|
||||
/// The current value of the form field.
|
||||
T get value => _value;
|
||||
@ -371,8 +430,10 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
void reset() {
|
||||
setState(() {
|
||||
_value = widget.initialValue;
|
||||
_hasInteractedByUser = false;
|
||||
_errorText = null;
|
||||
});
|
||||
Form.of(context)?._fieldDidChange();
|
||||
}
|
||||
|
||||
/// Calls [FormField.validator] to set the [errorText]. Returns true if there
|
||||
@ -397,11 +458,13 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
/// Updates this field's state to the new value. Useful for responding to
|
||||
/// child widget changes, e.g. [Slider]'s [Slider.onChanged] argument.
|
||||
///
|
||||
/// Triggers the [Form.onChanged] callback and, if the [Form.autovalidate]
|
||||
/// field is set, revalidates all the fields of the form.
|
||||
/// Triggers the [Form.onChanged] callback and, if [Form.autovalidateMode] is
|
||||
/// [AutovalidateMode.always] or [AutovalidateMode.onUserInteraction],
|
||||
/// revalidates all the fields of the form.
|
||||
void didChange(T value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
_hasInteractedByUser = true;
|
||||
});
|
||||
Form.of(context)?._fieldDidChange();
|
||||
}
|
||||
@ -432,10 +495,34 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Only autovalidate if the widget is also enabled
|
||||
if (widget.autovalidate && widget.enabled)
|
||||
_validate();
|
||||
if (widget.enabled) {
|
||||
switch (widget.autovalidateMode) {
|
||||
case AutovalidateMode.always:
|
||||
_validate();
|
||||
break;
|
||||
case AutovalidateMode.onUserInteraction:
|
||||
if (_hasInteractedByUser) {
|
||||
_validate();
|
||||
}
|
||||
break;
|
||||
case AutovalidateMode.disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
Form.of(context)?._register(this);
|
||||
return widget.builder(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to configure the auto validation of [FormField] and [Form] widgets.
|
||||
enum AutovalidateMode {
|
||||
/// No auto validation will occur.
|
||||
disabled,
|
||||
|
||||
/// Used to auto-validate [Form] and [FormField] even without user interaction.
|
||||
always,
|
||||
|
||||
/// Used to auto-validate [Form] and [FormField] only after each user
|
||||
/// interaction.
|
||||
onUserInteraction,
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ Finder _iconRichText(Key iconKey) {
|
||||
|
||||
Widget buildFormFrame({
|
||||
Key buttonKey,
|
||||
bool autovalidate = false,
|
||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
||||
int elevation = 8,
|
||||
String value = 'two',
|
||||
ValueChanged<String> onChanged,
|
||||
@ -55,7 +55,7 @@ Widget buildFormFrame({
|
||||
child: RepaintBoundary(
|
||||
child: DropdownButtonFormField<String>(
|
||||
key: buttonKey,
|
||||
autovalidate: autovalidate,
|
||||
autovalidateMode: autovalidateMode,
|
||||
elevation: elevation,
|
||||
value: value,
|
||||
hint: hint,
|
||||
@ -180,7 +180,7 @@ void main() {
|
||||
_validateCalled++;
|
||||
return currentValue == null ? 'Must select value' : null;
|
||||
},
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -763,4 +763,57 @@ void main() {
|
||||
expect(currentValue, equals('one'));
|
||||
expect(find.text(currentValue), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
|
||||
int _validateCalled = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: DropdownButtonFormField<String>(
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
items: menuItems.map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
validator: (String value) {
|
||||
_validateCalled++;
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_validateCalled, 1);
|
||||
});
|
||||
|
||||
testWidgets('autovalidateMode and autovalidate should not be used at the same time', (WidgetTester tester) async {
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: DropdownButtonFormField<String>(
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
items: menuItems.map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(() => builder(), throwsAssertionError);
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -2169,6 +2169,7 @@ void main() {
|
||||
icon: Container(),
|
||||
items: itemValues.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
|
@ -191,7 +191,7 @@ void main() {
|
||||
expect(_value, 'Soup');
|
||||
});
|
||||
|
||||
testWidgets('autovalidate is passed to super', (WidgetTester tester) async {
|
||||
testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
|
||||
int _validateCalled = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
@ -199,7 +199,7 @@ void main() {
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextFormField(
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: (String value) {
|
||||
_validateCalled++;
|
||||
return null;
|
||||
@ -225,7 +225,7 @@ void main() {
|
||||
child: Center(
|
||||
child: TextFormField(
|
||||
enabled: true,
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: (String value) {
|
||||
_validateCalled += 1;
|
||||
return null;
|
||||
@ -444,4 +444,46 @@ void main() {
|
||||
final TextField widget = tester.widget(find.byType(TextField));
|
||||
expect(widget.autofillHints, equals(const <String>[AutofillHints.countryName]));
|
||||
});
|
||||
|
||||
testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
|
||||
int _validateCalled = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Scaffold(
|
||||
body: TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (String value) {
|
||||
_validateCalled++;
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_validateCalled, 0);
|
||||
await tester.enterText(find.byType(TextField), 'a');
|
||||
await tester.pump();
|
||||
expect(_validateCalled, 1);
|
||||
});
|
||||
|
||||
testWidgets('autovalidateMode and autovalidate should not be used at the same time', (WidgetTester tester) async {
|
||||
expect(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Scaffold(
|
||||
body: TextFormField(
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, throwsAssertionError);
|
||||
});
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ void main() {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
String errorText(String value) => value + '/error';
|
||||
|
||||
Widget builder(bool autovalidate) {
|
||||
Widget builder(AutovalidateMode autovalidateMode) {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
@ -99,7 +99,7 @@ void main() {
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
autovalidate: autovalidate,
|
||||
autovalidateMode: autovalidateMode,
|
||||
child: TextFormField(
|
||||
validator: errorText,
|
||||
),
|
||||
@ -112,11 +112,11 @@ void main() {
|
||||
}
|
||||
|
||||
// Start off not autovalidating.
|
||||
await tester.pumpWidget(builder(false));
|
||||
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
||||
|
||||
Future<void> checkErrorText(String testValue) async {
|
||||
formKey.currentState.reset();
|
||||
await tester.pumpWidget(builder(false));
|
||||
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
||||
await tester.enterText(find.byType(TextFormField), testValue);
|
||||
await tester.pump();
|
||||
|
||||
@ -128,7 +128,7 @@ void main() {
|
||||
|
||||
// Try again with autovalidation. Should validate immediately.
|
||||
formKey.currentState.reset();
|
||||
await tester.pumpWidget(builder(true));
|
||||
await tester.pumpWidget(builder(AutovalidateMode.always));
|
||||
await tester.enterText(find.byType(TextFormField), testValue);
|
||||
await tester.pump();
|
||||
|
||||
@ -160,13 +160,13 @@ void main() {
|
||||
key: fieldKey1,
|
||||
initialValue: validString,
|
||||
validator: validator,
|
||||
autovalidate: true
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
),
|
||||
TextFormField(
|
||||
key: fieldKey2,
|
||||
initialValue: validString,
|
||||
validator: validator,
|
||||
autovalidate: true
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -207,13 +207,13 @@ void main() {
|
||||
key: fieldKey1,
|
||||
initialValue: validString,
|
||||
validator: validator,
|
||||
autovalidate: false,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
TextFormField(
|
||||
key: fieldKey2,
|
||||
initialValue: '',
|
||||
validator: validator,
|
||||
autovalidate: false,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -249,7 +249,7 @@ void main() {
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
@ -580,4 +580,273 @@ void main() {
|
||||
formKey.currentState.save();
|
||||
expect(formKey.currentState.validate(), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Does not auto-validate before value changes when autovalidateMode is set to onUserInteraction', (WidgetTester tester) async {
|
||||
FormFieldState<String> formFieldState;
|
||||
|
||||
String errorText(String value) => '$value/error';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: FormField<String>(
|
||||
initialValue: 'foo',
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
builder: (FormFieldState<String> state) {
|
||||
formFieldState = state;
|
||||
return Container();
|
||||
},
|
||||
validator: errorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// The form field has no error.
|
||||
expect(formFieldState.hasError, isFalse);
|
||||
// No error widget is visible.
|
||||
expect(find.text(errorText('foo')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('auto-validate before value changes if autovalidateMode was set to always', (WidgetTester tester) async {
|
||||
FormFieldState<String> formFieldState;
|
||||
|
||||
String errorText(String value) => '$value/error';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: FormField<String>(
|
||||
initialValue: 'foo',
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
builder: (FormFieldState<String> state) {
|
||||
formFieldState = state;
|
||||
return Container();
|
||||
},
|
||||
validator: errorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
expect(formFieldState.hasError, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Form auto-validates form fields only after one of them changes if autovalidateMode is onUserInteraction', (WidgetTester tester) async {
|
||||
const String initialValue = 'foo';
|
||||
String errorText(String value) => 'error/$value';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
initialValue: initialValue,
|
||||
validator: errorText,
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: initialValue,
|
||||
validator: errorText,
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: initialValue,
|
||||
validator: errorText,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Makes sure the Form widget won't autovalidate the form fields
|
||||
// after rebuilds if there is not user interaction.
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
// We expect no validation error text being shown.
|
||||
expect(find.text(errorText(initialValue)), findsNothing);
|
||||
|
||||
// Set a empty string into the first form field to
|
||||
// trigger the fields validators.
|
||||
await tester.enterText(find.byType(TextFormField).first, '');
|
||||
await tester.pump();
|
||||
|
||||
// Now we expect the errors to be shown for the first Text Field and
|
||||
// for the next two form fields that have their contents unchanged.
|
||||
expect(find.text(errorText('')), findsOneWidget);
|
||||
expect(find.text(errorText(initialValue)), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Form auto-validates form fields even before any have changed if autovalidateMode is set to always', (WidgetTester tester) async {
|
||||
String errorText(String value) => 'error/$value';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: TextFormField(
|
||||
validator: errorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// The issue only happens on the second build so we
|
||||
// need to rebuild the tree twice.
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
// We expect validation error text being shown.
|
||||
expect(find.text(errorText('')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('autovalidate parameter is still used if true', (WidgetTester tester) async {
|
||||
FormFieldState<String> formFieldState;
|
||||
String errorText(String value) => '$value/error';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: FormField<String>(
|
||||
initialValue: 'foo',
|
||||
autovalidate: true,
|
||||
builder: (FormFieldState<String> state) {
|
||||
formFieldState = state;
|
||||
return Container();
|
||||
},
|
||||
validator: errorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
expect(formFieldState.hasError, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Form.reset() resets form fields, and auto validation will only happen on the next user interaction if autovalidateMode is onUserInteraction', (WidgetTester tester) async {
|
||||
final GlobalKey<FormState> formState = GlobalKey<FormState>();
|
||||
String errorText(String value) => '$value/error';
|
||||
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(),
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Form(
|
||||
key: formState,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Material(
|
||||
child: TextFormField(
|
||||
initialValue: 'foo',
|
||||
validator: errorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
// No error text is visible yet.
|
||||
expect(find.text(errorText('foo')), findsNothing);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), 'bar');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
expect(find.text(errorText('bar')), findsOneWidget);
|
||||
|
||||
// Resetting the form state should remove the error text.
|
||||
formState.currentState.reset();
|
||||
await tester.pump();
|
||||
expect(find.text(errorText('bar')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Form.autovalidateMode and Form.autovalidate should not be used at the same time', (WidgetTester tester) async {
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Form(
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
child: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(() => builder(), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('FormField.autovalidateMode and FormField.autovalidate should not be used at the same time', (WidgetTester tester) async {
|
||||
Widget builder() {
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: FormField<String>(
|
||||
autovalidate: true,
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
builder: (_) {
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(() => builder(), throwsAssertionError);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user