mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Forms provide more control over when they validate. (#7283)
Callers can manually validate by calling validate(), or tell the Form to validate on every change by setting the `autovalidate` parameter. Fixes https://github.com/flutter/flutter/issues/7219
This commit is contained in:
parent
0d746ff155
commit
6d4191e98c
@ -30,11 +30,13 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
));
|
||||
}
|
||||
|
||||
bool _autovalidate = false;
|
||||
GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
||||
GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>();
|
||||
void _handleSubmitted() {
|
||||
FormState form = _formKey.currentState;
|
||||
if (form.hasErrors) {
|
||||
if (!form.validate()) {
|
||||
_autovalidate = true; // Start validating on every change.
|
||||
showInSnackBar('Please fix the errors in red before submitting.');
|
||||
} else {
|
||||
form.save();
|
||||
@ -76,6 +78,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
),
|
||||
body: new Form(
|
||||
key: _formKey,
|
||||
autovalidate: _autovalidate,
|
||||
child: new Block(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
children: <Widget>[
|
||||
|
||||
@ -22,6 +22,7 @@ class Form extends StatefulWidget {
|
||||
Form({
|
||||
Key key,
|
||||
@required this.child,
|
||||
this.autovalidate: false,
|
||||
}) : super(key: key) {
|
||||
assert(child != null);
|
||||
}
|
||||
@ -42,6 +43,11 @@ class Form extends StatefulWidget {
|
||||
/// Root of the widget hierarchy that contains this form.
|
||||
final Widget child;
|
||||
|
||||
/// If true, form fields will validate and update their error text
|
||||
/// immediately after every change. Otherwise, you must call
|
||||
/// [FormState.validate] to validate.
|
||||
final bool autovalidate;
|
||||
|
||||
@override
|
||||
FormState createState() => new FormState();
|
||||
}
|
||||
@ -68,6 +74,8 @@ class FormState extends State<Form> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (config.autovalidate)
|
||||
_validate();
|
||||
return new _FormScope(
|
||||
formState: this,
|
||||
generation: _generation,
|
||||
@ -75,13 +83,13 @@ class FormState extends State<Form> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Saves every FormField that is a descendant of this Form.
|
||||
/// Saves every [FormField] that is a descendant of this [Form].
|
||||
void save() {
|
||||
for (FormFieldState<dynamic> field in _fields)
|
||||
field.save();
|
||||
}
|
||||
|
||||
/// Resets every FormField that is a descendant of this Form back to its
|
||||
/// Resets every [FormField] that is a descendant of this [Form] back to its
|
||||
/// initialState.
|
||||
void reset() {
|
||||
for (FormFieldState<dynamic> field in _fields)
|
||||
@ -89,13 +97,18 @@ class FormState extends State<Form> {
|
||||
_fieldDidChange();
|
||||
}
|
||||
|
||||
/// Returns true if any descendant FormField has an error, false otherwise.
|
||||
bool get hasErrors {
|
||||
for (FormFieldState<dynamic> field in _fields) {
|
||||
if (field.hasError)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
/// Validates every [FormField] that is a descendant of this [Form], and
|
||||
/// returns true iff there are no errors.
|
||||
bool validate() {
|
||||
_fieldDidChange();
|
||||
return _validate();
|
||||
}
|
||||
|
||||
bool _validate() {
|
||||
bool hasError = false;
|
||||
for (FormFieldState<dynamic> field in _fields)
|
||||
hasError = !field.validate() || hasError;
|
||||
return !hasError;
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,6 +174,7 @@ class FormField<T> extends StatefulWidget {
|
||||
this.onSaved,
|
||||
this.validator,
|
||||
this.initialValue,
|
||||
this.autovalidate: false,
|
||||
}) : super(key: key) {
|
||||
assert(builder != null);
|
||||
}
|
||||
@ -181,6 +195,12 @@ class FormField<T> extends StatefulWidget {
|
||||
/// An optional value to initialize the form field to, or null otherwise.
|
||||
final T initialValue;
|
||||
|
||||
/// If true, this form fields will validate and update its error text
|
||||
/// immediately after every change. Otherwise, you must call
|
||||
/// [FormFieldState.validate] to validate. If part of a [Form] that
|
||||
/// autovalidates, this value will be ignored.
|
||||
final bool autovalidate;
|
||||
|
||||
@override
|
||||
FormFieldState<T> createState() => new FormFieldState<T>();
|
||||
}
|
||||
@ -194,8 +214,9 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
/// The current value of the form field.
|
||||
T get value => _value;
|
||||
|
||||
/// The current validation error returned by [FormField]'s [validator]
|
||||
/// callback, or null if no errors.
|
||||
/// The current validation error returned by the [FormField.validator]
|
||||
/// callback, or null if no errors have been triggered. This only updates when
|
||||
/// [validate] is called.
|
||||
String get errorText => _errorText;
|
||||
|
||||
/// True if this field has any validation errors.
|
||||
@ -215,6 +236,21 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Calls [FormField.validator] to set the [errorText]. Returns true if there
|
||||
/// were no errors.
|
||||
bool validate() {
|
||||
setState(() {
|
||||
_validate();
|
||||
});
|
||||
return !hasError;
|
||||
}
|
||||
|
||||
bool _validate() {
|
||||
if (config.validator != null)
|
||||
_errorText = config.validator(_value);
|
||||
return !hasError;
|
||||
}
|
||||
|
||||
/// Updates this field's state to the new value. Useful for responding to
|
||||
/// child widget changes, e.g. [Slider]'s onChanged argument.
|
||||
void onChanged(T value) {
|
||||
@ -238,9 +274,8 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (config.validator != null)
|
||||
_errorText = config.validator(_value);
|
||||
|
||||
if (config.autovalidate)
|
||||
_validate();
|
||||
Form.of(context)?._register(this);
|
||||
return config.builder(this);
|
||||
}
|
||||
|
||||
@ -54,14 +54,17 @@ void main() {
|
||||
await checkText('');
|
||||
});
|
||||
|
||||
testWidgets('Validator sets the error text', (WidgetTester tester) async {
|
||||
testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
|
||||
GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
||||
GlobalKey inputKey = new GlobalKey();
|
||||
String errorText(InputValue input) => input.text + '/error';
|
||||
|
||||
Widget builder() {
|
||||
Widget builder(bool autovalidate) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Form(
|
||||
key: formKey,
|
||||
autovalidate: autovalidate,
|
||||
child: new InputFormField(
|
||||
key: inputKey,
|
||||
validator: errorText,
|
||||
@ -71,14 +74,28 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// Start off not autovalidating.
|
||||
await tester.pumpWidget(builder(false));
|
||||
await showKeyboard(tester);
|
||||
|
||||
Future<Null> checkErrorText(String testValue) async {
|
||||
formKey.currentState.reset();
|
||||
enterText(testValue);
|
||||
await tester.idle();
|
||||
await tester.pumpWidget(builder(false));
|
||||
|
||||
// We have to manually validate if we're not autovalidating.
|
||||
expect(find.text(errorText(new InputValue(text: testValue))), findsNothing);
|
||||
formKey.currentState.validate();
|
||||
await tester.pump();
|
||||
// Check for a new Text widget with our error text.
|
||||
expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget);
|
||||
|
||||
// Try again with autovalidation. Should validate immediately.
|
||||
formKey.currentState.reset();
|
||||
enterText(testValue);
|
||||
await tester.idle();
|
||||
await tester.pumpWidget(builder(true));
|
||||
|
||||
expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget);
|
||||
}
|
||||
|
||||
@ -98,6 +115,7 @@ void main() {
|
||||
child: new Material(
|
||||
child: new Form(
|
||||
key: formKey,
|
||||
autovalidate: true,
|
||||
child: new Focus(
|
||||
key: focusKey,
|
||||
child: new Block(
|
||||
@ -195,21 +213,21 @@ void main() {
|
||||
await showKeyboard(tester);
|
||||
|
||||
expect(fieldValue, isNull);
|
||||
expect(formKey.currentState.hasErrors, isFalse);
|
||||
expect(formKey.currentState.validate(), isTrue);
|
||||
|
||||
enterText('Test');
|
||||
await tester.idle();
|
||||
await tester.pumpWidget(builder(false));
|
||||
|
||||
// Form wasn't saved, but validator runs immediately.
|
||||
// Form wasn't saved yet.
|
||||
expect(fieldValue, null);
|
||||
expect(formKey.currentState.hasErrors, isTrue);
|
||||
expect(formKey.currentState.validate(), isFalse);
|
||||
|
||||
formKey.currentState.save();
|
||||
|
||||
// Now fieldValue is saved.
|
||||
expect(fieldValue, 'Test');
|
||||
expect(formKey.currentState.hasErrors, isTrue);
|
||||
expect(formKey.currentState.validate(), isFalse);
|
||||
|
||||
// Now remove the field with an error.
|
||||
await tester.pumpWidget(builder(true));
|
||||
@ -217,6 +235,6 @@ void main() {
|
||||
// Reset the form. Should not crash.
|
||||
formKey.currentState.reset();
|
||||
formKey.currentState.save();
|
||||
expect(formKey.currentState.hasErrors, isFalse);
|
||||
expect(formKey.currentState.validate(), isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user