Commit 6d4191e9 authored by Matt Perry's avatar Matt Perry Committed by GitHub

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
parent 0d746ff1
......@@ -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);
});
}
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