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> { ...@@ -1437,7 +1437,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
/// Creates a [DropdownButton] widget that is a [FormField], wrapped in an /// Creates a [DropdownButton] widget that is a [FormField], wrapped in an
/// [InputDecorator]. /// [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 /// parameters, see [FormField]. For the rest (other than [decoration]), see
/// [DropdownButton]. /// [DropdownButton].
/// ///
...@@ -1469,6 +1469,7 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1469,6 +1469,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
FormFieldSetter<T> onSaved, FormFieldSetter<T> onSaved,
FormFieldValidator<T> validator, FormFieldValidator<T> validator,
bool autovalidate = false, bool autovalidate = false,
AutovalidateMode autovalidateMode,
}) : assert(items == null || items.isEmpty || value == null || }) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) { items.where((DropdownMenuItem<T> item) {
return item.value == value; return item.value == value;
...@@ -1484,13 +1485,21 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1484,13 +1485,21 @@ class DropdownButtonFormField<T> extends FormField<T> {
assert(isExpanded != null), assert(isExpanded != null),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension), assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
assert(autofocus != null), 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), decoration = decoration ?? InputDecoration(focusColor: focusColor),
super( super(
key: key, key: key,
onSaved: onSaved, onSaved: onSaved,
initialValue: value, initialValue: value,
validator: validator, validator: validator,
autovalidate: autovalidate, autovalidateMode: autovalidate
? AutovalidateMode.always
: (autovalidateMode ?? AutovalidateMode.disabled),
builder: (FormFieldState<T> field) { builder: (FormFieldState<T> field) {
final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>; final _DropdownButtonFormFieldState<T> state = field as _DropdownButtonFormFieldState<T>;
final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor); final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor);
......
...@@ -180,6 +180,7 @@ class TextFormField extends FormField<String> { ...@@ -180,6 +180,7 @@ class TextFormField extends FormField<String> {
InputCounterWidgetBuilder buildCounter, InputCounterWidgetBuilder buildCounter,
ScrollPhysics scrollPhysics, ScrollPhysics scrollPhysics,
Iterable<String> autofillHints, Iterable<String> autofillHints,
AutovalidateMode autovalidateMode,
}) : assert(initialValue == null || controller == null), }) : assert(initialValue == null || controller == null),
assert(textAlign != null), assert(textAlign != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -189,6 +190,11 @@ class TextFormField extends FormField<String> { ...@@ -189,6 +190,11 @@ class TextFormField extends FormField<String> {
assert(autocorrect != null), assert(autocorrect != null),
assert(enableSuggestions != null), assert(enableSuggestions != null),
assert(autovalidate != null), assert(autovalidate != null),
assert(
autovalidate == false ||
autovalidate == true && autovalidateMode == null,
'autovalidate and autovalidateMode should not be used together.'
),
assert(maxLengthEnforced != null), assert(maxLengthEnforced != null),
assert(scrollPadding != null), assert(scrollPadding != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
...@@ -206,67 +212,69 @@ class TextFormField extends FormField<String> { ...@@ -206,67 +212,69 @@ class TextFormField extends FormField<String> {
assert(maxLength == null || maxLength > 0), assert(maxLength == null || maxLength > 0),
assert(enableInteractiveSelection != null), assert(enableInteractiveSelection != null),
super( super(
key: key, key: key,
initialValue: controller != null ? controller.text : (initialValue ?? ''), initialValue: controller != null ? controller.text : (initialValue ?? ''),
onSaved: onSaved, onSaved: onSaved,
validator: validator, validator: validator,
autovalidate: autovalidate, enabled: enabled ?? decoration?.enabled ?? true,
enabled: enabled ?? decoration?.enabled ?? true, autovalidateMode: autovalidate
builder: (FormFieldState<String> field) { ? AutovalidateMode.always
final _TextFormFieldState state = field as _TextFormFieldState; : (autovalidateMode ?? AutovalidateMode.disabled),
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration()) builder: (FormFieldState<String> field) {
.applyDefaults(Theme.of(field.context).inputDecorationTheme); final _TextFormFieldState state = field as _TextFormFieldState;
void onChangedHandler(String value) { final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
if (onChanged != null) { .applyDefaults(Theme.of(field.context).inputDecorationTheme);
onChanged(value); void onChangedHandler(String value) {
} if (onChanged != null) {
field.didChange(value); onChanged(value);
} }
return TextField( field.didChange(value);
controller: state._effectiveController, }
focusNode: focusNode, return TextField(
decoration: effectiveDecoration.copyWith(errorText: field.errorText), controller: state._effectiveController,
keyboardType: keyboardType, focusNode: focusNode,
textInputAction: textInputAction, decoration: effectiveDecoration.copyWith(errorText: field.errorText),
style: style, keyboardType: keyboardType,
strutStyle: strutStyle, textInputAction: textInputAction,
textAlign: textAlign, style: style,
textAlignVertical: textAlignVertical, strutStyle: strutStyle,
textDirection: textDirection, textAlign: textAlign,
textCapitalization: textCapitalization, textAlignVertical: textAlignVertical,
autofocus: autofocus, textDirection: textDirection,
toolbarOptions: toolbarOptions, textCapitalization: textCapitalization,
readOnly: readOnly, autofocus: autofocus,
showCursor: showCursor, toolbarOptions: toolbarOptions,
obscuringCharacter: obscuringCharacter, readOnly: readOnly,
obscureText: obscureText, showCursor: showCursor,
autocorrect: autocorrect, obscuringCharacter: obscuringCharacter,
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), obscureText: obscureText,
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), autocorrect: autocorrect,
enableSuggestions: enableSuggestions, smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
maxLengthEnforced: maxLengthEnforced, smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
maxLines: maxLines, enableSuggestions: enableSuggestions,
minLines: minLines, maxLengthEnforced: maxLengthEnforced,
expands: expands, maxLines: maxLines,
maxLength: maxLength, minLines: minLines,
onChanged: onChangedHandler, expands: expands,
onTap: onTap, maxLength: maxLength,
onEditingComplete: onEditingComplete, onChanged: onChangedHandler,
onSubmitted: onFieldSubmitted, onTap: onTap,
inputFormatters: inputFormatters, onEditingComplete: onEditingComplete,
enabled: enabled ?? decoration?.enabled ?? true, onSubmitted: onFieldSubmitted,
cursorWidth: cursorWidth, inputFormatters: inputFormatters,
cursorRadius: cursorRadius, enabled: enabled ?? decoration?.enabled ?? true,
cursorColor: cursorColor, cursorWidth: cursorWidth,
scrollPadding: scrollPadding, cursorRadius: cursorRadius,
scrollPhysics: scrollPhysics, cursorColor: cursorColor,
keyboardAppearance: keyboardAppearance, scrollPadding: scrollPadding,
enableInteractiveSelection: enableInteractiveSelection, scrollPhysics: scrollPhysics,
buildCounter: buildCounter, keyboardAppearance: keyboardAppearance,
autofillHints: autofillHints, enableInteractiveSelection: enableInteractiveSelection,
); buildCounter: buildCounter,
}, autofillHints: autofillHints,
); );
},
);
/// Controls the text being edited. /// Controls the text being edited.
/// ///
......
...@@ -81,7 +81,17 @@ class Form extends StatefulWidget { ...@@ -81,7 +81,17 @@ class Form extends StatefulWidget {
this.autovalidate = false, this.autovalidate = false,
this.onWillPop, this.onWillPop,
this.onChanged, this.onChanged,
AutovalidateMode autovalidateMode,
}) : assert(child != null), }) : 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); super(key: key);
/// Returns the closest [FormState] which encloses the given context. /// Returns the closest [FormState] which encloses the given context.
...@@ -127,6 +137,12 @@ class Form extends StatefulWidget { ...@@ -127,6 +137,12 @@ class Form extends StatefulWidget {
/// will rebuild. /// will rebuild.
final VoidCallback onChanged; 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 @override
FormState createState() => FormState(); FormState createState() => FormState();
} }
...@@ -139,6 +155,7 @@ class Form extends StatefulWidget { ...@@ -139,6 +155,7 @@ class Form extends StatefulWidget {
/// Typically obtained via [Form.of]. /// Typically obtained via [Form.of].
class FormState extends State<Form> { class FormState extends State<Form> {
int _generation = 0; int _generation = 0;
bool _hasInteractedByUser = false;
final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{}; final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{};
// Called when a form field has changed. This will cause all form fields // Called when a form field has changed. This will cause all form fields
...@@ -146,6 +163,10 @@ class FormState extends State<Form> { ...@@ -146,6 +163,10 @@ class FormState extends State<Form> {
void _fieldDidChange() { void _fieldDidChange() {
if (widget.onChanged != null) if (widget.onChanged != null)
widget.onChanged(); widget.onChanged();
_hasInteractedByUser = _fields
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser);
_forceRebuild(); _forceRebuild();
} }
...@@ -165,8 +186,19 @@ class FormState extends State<Form> { ...@@ -165,8 +186,19 @@ class FormState extends State<Form> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.autovalidate) switch (widget.autovalidateMode) {
_validate(); case AutovalidateMode.always:
_validate();
break;
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
}
break;
case AutovalidateMode.disabled:
break;
}
return WillPopScope( return WillPopScope(
onWillPop: widget.onWillPop, onWillPop: widget.onWillPop,
child: _FormScope( child: _FormScope(
...@@ -188,11 +220,12 @@ class FormState extends State<Form> { ...@@ -188,11 +220,12 @@ class FormState extends State<Form> {
/// ///
/// The [Form.onChanged] callback will be called. /// The [Form.onChanged] callback will be called.
/// ///
/// If the form's [Form.autovalidate] property is true, the fields will all be /// If the form's [Form.autovalidateMode] property is [AutovalidateMode.always],
/// revalidated after being reset. /// the fields will all be revalidated after being reset.
void reset() { void reset() {
for (final FormFieldState<dynamic> field in _fields) for (final FormFieldState<dynamic> field in _fields)
field.reset(); field.reset();
_hasInteractedByUser = false;
_fieldDidChange(); _fieldDidChange();
} }
...@@ -201,6 +234,7 @@ class FormState extends State<Form> { ...@@ -201,6 +234,7 @@ class FormState extends State<Form> {
/// ///
/// The form will rebuild to report the results. /// The form will rebuild to report the results.
bool validate() { bool validate() {
_hasInteractedByUser = true;
_forceRebuild(); _forceRebuild();
return _validate(); return _validate();
} }
...@@ -287,7 +321,16 @@ class FormField<T> extends StatefulWidget { ...@@ -287,7 +321,16 @@ class FormField<T> extends StatefulWidget {
this.initialValue, this.initialValue,
this.autovalidate = false, this.autovalidate = false,
this.enabled = true, this.enabled = true,
AutovalidateMode autovalidateMode,
}) : assert(builder != null), }) : 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); super(key: key);
/// An optional method to call with the final value when the form is saved via /// An optional method to call with the final value when the form is saved via
...@@ -325,11 +368,26 @@ class FormField<T> extends StatefulWidget { ...@@ -325,11 +368,26 @@ class FormField<T> extends StatefulWidget {
/// Whether the form is able to receive user input. /// Whether the form is able to receive user input.
/// ///
/// Defaults to true. If [autovalidate] is true, the field will be validated. /// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled],
/// Likewise, if this field is false, the widget will not be validated /// the field will be auto validated. Likewise, if this field is false, the widget
/// regardless of [autovalidate]. /// will not be validated regardless of [autovalidateMode].
final bool enabled; 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 @override
FormFieldState<T> createState() => FormFieldState<T>(); FormFieldState<T> createState() => FormFieldState<T>();
} }
...@@ -339,6 +397,7 @@ class FormField<T> extends StatefulWidget { ...@@ -339,6 +397,7 @@ class FormField<T> extends StatefulWidget {
class FormFieldState<T> extends State<FormField<T>> { class FormFieldState<T> extends State<FormField<T>> {
T _value; T _value;
String _errorText; String _errorText;
bool _hasInteractedByUser = false;
/// The current value of the form field. /// The current value of the form field.
T get value => _value; T get value => _value;
...@@ -371,8 +430,10 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -371,8 +430,10 @@ class FormFieldState<T> extends State<FormField<T>> {
void reset() { void reset() {
setState(() { setState(() {
_value = widget.initialValue; _value = widget.initialValue;
_hasInteractedByUser = false;
_errorText = null; _errorText = null;
}); });
Form.of(context)?._fieldDidChange();
} }
/// Calls [FormField.validator] to set the [errorText]. Returns true if there /// Calls [FormField.validator] to set the [errorText]. Returns true if there
...@@ -397,11 +458,13 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -397,11 +458,13 @@ class FormFieldState<T> extends State<FormField<T>> {
/// Updates this field's state to the new value. Useful for responding to /// Updates this field's state to the new value. Useful for responding to
/// child widget changes, e.g. [Slider]'s [Slider.onChanged] argument. /// child widget changes, e.g. [Slider]'s [Slider.onChanged] argument.
/// ///
/// Triggers the [Form.onChanged] callback and, if the [Form.autovalidate] /// Triggers the [Form.onChanged] callback and, if [Form.autovalidateMode] is
/// field is set, revalidates all the fields of the form. /// [AutovalidateMode.always] or [AutovalidateMode.onUserInteraction],
/// revalidates all the fields of the form.
void didChange(T value) { void didChange(T value) {
setState(() { setState(() {
_value = value; _value = value;
_hasInteractedByUser = true;
}); });
Form.of(context)?._fieldDidChange(); Form.of(context)?._fieldDidChange();
} }
...@@ -432,10 +495,34 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -432,10 +495,34 @@ class FormFieldState<T> extends State<FormField<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Only autovalidate if the widget is also enabled if (widget.enabled) {
if (widget.autovalidate && widget.enabled) switch (widget.autovalidateMode) {
_validate(); case AutovalidateMode.always:
_validate();
break;
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
}
break;
case AutovalidateMode.disabled:
break;
}
}
Form.of(context)?._register(this); Form.of(context)?._register(this);
return widget.builder(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) { ...@@ -29,7 +29,7 @@ Finder _iconRichText(Key iconKey) {
Widget buildFormFrame({ Widget buildFormFrame({
Key buttonKey, Key buttonKey,
bool autovalidate = false, AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
int elevation = 8, int elevation = 8,
String value = 'two', String value = 'two',
ValueChanged<String> onChanged, ValueChanged<String> onChanged,
...@@ -55,7 +55,7 @@ Widget buildFormFrame({ ...@@ -55,7 +55,7 @@ Widget buildFormFrame({
child: RepaintBoundary( child: RepaintBoundary(
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
key: buttonKey, key: buttonKey,
autovalidate: autovalidate, autovalidateMode: autovalidateMode,
elevation: elevation, elevation: elevation,
value: value, value: value,
hint: hint, hint: hint,
...@@ -180,7 +180,7 @@ void main() { ...@@ -180,7 +180,7 @@ void main() {
_validateCalled++; _validateCalled++;
return currentValue == null ? 'Must select value' : null; return currentValue == null ? 'Must select value' : null;
}, },
autovalidate: true, autovalidateMode: AutovalidateMode.always,
), ),
), ),
); );
...@@ -763,4 +763,57 @@ void main() { ...@@ -763,4 +763,57 @@ void main() {
expect(currentValue, equals('one')); expect(currentValue, equals('one'));
expect(find.text(currentValue), findsOneWidget); 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() { ...@@ -2169,6 +2169,7 @@ void main() {
icon: Container(), icon: Container(),
items: itemValues.map<DropdownMenuItem<String>>((String value) { items: itemValues.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: value,
child: Text(value), child: Text(value),
); );
}).toList(), }).toList(),
......
...@@ -191,7 +191,7 @@ void main() { ...@@ -191,7 +191,7 @@ void main() {
expect(_value, 'Soup'); expect(_value, 'Soup');
}); });
testWidgets('autovalidate is passed to super', (WidgetTester tester) async { testWidgets('autovalidateMode is passed to super', (WidgetTester tester) async {
int _validateCalled = 0; int _validateCalled = 0;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -199,7 +199,7 @@ void main() { ...@@ -199,7 +199,7 @@ void main() {
home: Material( home: Material(
child: Center( child: Center(
child: TextFormField( child: TextFormField(
autovalidate: true, autovalidateMode: AutovalidateMode.always,
validator: (String value) { validator: (String value) {
_validateCalled++; _validateCalled++;
return null; return null;
...@@ -225,7 +225,7 @@ void main() { ...@@ -225,7 +225,7 @@ void main() {
child: Center( child: Center(
child: TextFormField( child: TextFormField(
enabled: true, enabled: true,
autovalidate: true, autovalidateMode: AutovalidateMode.always,
validator: (String value) { validator: (String value) {
_validateCalled += 1; _validateCalled += 1;
return null; return null;
...@@ -444,4 +444,46 @@ void main() { ...@@ -444,4 +444,46 @@ void main() {
final TextField widget = tester.widget(find.byType(TextField)); final TextField widget = tester.widget(find.byType(TextField));
expect(widget.autofillHints, equals(const <String>[AutofillHints.countryName])); 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