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> { ...@@ -30,11 +30,13 @@ class TextFieldDemoState extends State<TextFieldDemo> {
)); ));
} }
bool _autovalidate = false;
GlobalKey<FormState> _formKey = new GlobalKey<FormState>(); GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>(); GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>();
void _handleSubmitted() { void _handleSubmitted() {
FormState form = _formKey.currentState; 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.'); showInSnackBar('Please fix the errors in red before submitting.');
} else { } else {
form.save(); form.save();
...@@ -76,6 +78,7 @@ class TextFieldDemoState extends State<TextFieldDemo> { ...@@ -76,6 +78,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
), ),
body: new Form( body: new Form(
key: _formKey, key: _formKey,
autovalidate: _autovalidate,
child: new Block( child: new Block(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: <Widget>[ children: <Widget>[
......
...@@ -22,6 +22,7 @@ class Form extends StatefulWidget { ...@@ -22,6 +22,7 @@ class Form extends StatefulWidget {
Form({ Form({
Key key, Key key,
@required this.child, @required this.child,
this.autovalidate: false,
}) : super(key: key) { }) : super(key: key) {
assert(child != null); assert(child != null);
} }
...@@ -42,6 +43,11 @@ class Form extends StatefulWidget { ...@@ -42,6 +43,11 @@ class Form extends StatefulWidget {
/// Root of the widget hierarchy that contains this form. /// Root of the widget hierarchy that contains this form.
final Widget child; 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 @override
FormState createState() => new FormState(); FormState createState() => new FormState();
} }
...@@ -68,6 +74,8 @@ class FormState extends State<Form> { ...@@ -68,6 +74,8 @@ class FormState extends State<Form> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (config.autovalidate)
_validate();
return new _FormScope( return new _FormScope(
formState: this, formState: this,
generation: _generation, generation: _generation,
...@@ -75,13 +83,13 @@ class FormState extends State<Form> { ...@@ -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() { void save() {
for (FormFieldState<dynamic> field in _fields) for (FormFieldState<dynamic> field in _fields)
field.save(); 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. /// initialState.
void reset() { void reset() {
for (FormFieldState<dynamic> field in _fields) for (FormFieldState<dynamic> field in _fields)
...@@ -89,13 +97,18 @@ class FormState extends State<Form> { ...@@ -89,13 +97,18 @@ class FormState extends State<Form> {
_fieldDidChange(); _fieldDidChange();
} }
/// Returns true if any descendant FormField has an error, false otherwise. /// Validates every [FormField] that is a descendant of this [Form], and
bool get hasErrors { /// returns true iff there are no errors.
for (FormFieldState<dynamic> field in _fields) { bool validate() {
if (field.hasError) _fieldDidChange();
return true; return _validate();
} }
return false;
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 { ...@@ -161,6 +174,7 @@ class FormField<T> extends StatefulWidget {
this.onSaved, this.onSaved,
this.validator, this.validator,
this.initialValue, this.initialValue,
this.autovalidate: false,
}) : super(key: key) { }) : super(key: key) {
assert(builder != null); assert(builder != null);
} }
...@@ -181,6 +195,12 @@ class FormField<T> extends StatefulWidget { ...@@ -181,6 +195,12 @@ class FormField<T> extends StatefulWidget {
/// An optional value to initialize the form field to, or null otherwise. /// An optional value to initialize the form field to, or null otherwise.
final T initialValue; 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 @override
FormFieldState<T> createState() => new FormFieldState<T>(); FormFieldState<T> createState() => new FormFieldState<T>();
} }
...@@ -194,8 +214,9 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -194,8 +214,9 @@ class FormFieldState<T> extends State<FormField<T>> {
/// The current value of the form field. /// The current value of the form field.
T get value => _value; T get value => _value;
/// The current validation error returned by [FormField]'s [validator] /// The current validation error returned by the [FormField.validator]
/// callback, or null if no errors. /// callback, or null if no errors have been triggered. This only updates when
/// [validate] is called.
String get errorText => _errorText; String get errorText => _errorText;
/// True if this field has any validation errors. /// True if this field has any validation errors.
...@@ -215,6 +236,21 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -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 /// Updates this field's state to the new value. Useful for responding to
/// child widget changes, e.g. [Slider]'s onChanged argument. /// child widget changes, e.g. [Slider]'s onChanged argument.
void onChanged(T value) { void onChanged(T value) {
...@@ -238,9 +274,8 @@ class FormFieldState<T> extends State<FormField<T>> { ...@@ -238,9 +274,8 @@ class FormFieldState<T> extends State<FormField<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (config.validator != null) if (config.autovalidate)
_errorText = config.validator(_value); _validate();
Form.of(context)?._register(this); Form.of(context)?._register(this);
return config.builder(this); return config.builder(this);
} }
......
...@@ -54,14 +54,17 @@ void main() { ...@@ -54,14 +54,17 @@ void main() {
await checkText(''); 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(); GlobalKey inputKey = new GlobalKey();
String errorText(InputValue input) => input.text + '/error'; String errorText(InputValue input) => input.text + '/error';
Widget builder() { Widget builder(bool autovalidate) {
return new Center( return new Center(
child: new Material( child: new Material(
child: new Form( child: new Form(
key: formKey,
autovalidate: autovalidate,
child: new InputFormField( child: new InputFormField(
key: inputKey, key: inputKey,
validator: errorText, validator: errorText,
...@@ -71,14 +74,28 @@ void main() { ...@@ -71,14 +74,28 @@ void main() {
); );
} }
await tester.pumpWidget(builder()); // Start off not autovalidating.
await tester.pumpWidget(builder(false));
await showKeyboard(tester); await showKeyboard(tester);
Future<Null> checkErrorText(String testValue) async { Future<Null> checkErrorText(String testValue) async {
formKey.currentState.reset();
enterText(testValue); enterText(testValue);
await tester.idle(); 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(); 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); expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget);
} }
...@@ -98,6 +115,7 @@ void main() { ...@@ -98,6 +115,7 @@ void main() {
child: new Material( child: new Material(
child: new Form( child: new Form(
key: formKey, key: formKey,
autovalidate: true,
child: new Focus( child: new Focus(
key: focusKey, key: focusKey,
child: new Block( child: new Block(
...@@ -195,21 +213,21 @@ void main() { ...@@ -195,21 +213,21 @@ void main() {
await showKeyboard(tester); await showKeyboard(tester);
expect(fieldValue, isNull); expect(fieldValue, isNull);
expect(formKey.currentState.hasErrors, isFalse); expect(formKey.currentState.validate(), isTrue);
enterText('Test'); enterText('Test');
await tester.idle(); await tester.idle();
await tester.pumpWidget(builder(false)); await tester.pumpWidget(builder(false));
// Form wasn't saved, but validator runs immediately. // Form wasn't saved yet.
expect(fieldValue, null); expect(fieldValue, null);
expect(formKey.currentState.hasErrors, isTrue); expect(formKey.currentState.validate(), isFalse);
formKey.currentState.save(); formKey.currentState.save();
// Now fieldValue is saved. // Now fieldValue is saved.
expect(fieldValue, 'Test'); expect(fieldValue, 'Test');
expect(formKey.currentState.hasErrors, isTrue); expect(formKey.currentState.validate(), isFalse);
// Now remove the field with an error. // Now remove the field with an error.
await tester.pumpWidget(builder(true)); await tester.pumpWidget(builder(true));
...@@ -217,6 +235,6 @@ void main() { ...@@ -217,6 +235,6 @@ void main() {
// Reset the form. Should not crash. // Reset the form. Should not crash.
formKey.currentState.reset(); formKey.currentState.reset();
formKey.currentState.save(); 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