Unverified Commit 5a69de82 authored by Pedro Massango's avatar Pedro Massango Committed by GitHub

FormField should autovalidate only if its content was changed (fixed) (#59766)

parent 9c4a5ef1
......@@ -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);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment