diff --git a/examples/flutter_gallery/lib/demo/text_field_demo.dart b/examples/flutter_gallery/lib/demo/text_field_demo.dart index 65ffbe63207..f82150f7573 100644 --- a/examples/flutter_gallery/lib/demo/text_field_demo.dart +++ b/examples/flutter_gallery/lib/demo/text_field_demo.dart @@ -30,11 +30,13 @@ class TextFieldDemoState extends State { )); } + bool _autovalidate = false; GlobalKey _formKey = new GlobalKey(); GlobalKey> _passwordFieldKey = new GlobalKey>(); 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 { ), body: new Form( key: _formKey, + autovalidate: _autovalidate, child: new Block( padding: const EdgeInsets.symmetric(horizontal: 16.0), children: [ diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index fdc91930090..8fba49696c6 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -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
{ @override Widget build(BuildContext context) { + if (config.autovalidate) + _validate(); return new _FormScope( formState: this, generation: _generation, @@ -75,13 +83,13 @@ class FormState extends State { ); } - /// Saves every FormField that is a descendant of this Form. + /// Saves every [FormField] that is a descendant of this [Form]. void save() { for (FormFieldState 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 field in _fields) @@ -89,13 +97,18 @@ class FormState extends State { _fieldDidChange(); } - /// Returns true if any descendant FormField has an error, false otherwise. - bool get hasErrors { - for (FormFieldState 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 field in _fields) + hasError = !field.validate() || hasError; + return !hasError; } } @@ -161,6 +174,7 @@ class FormField extends StatefulWidget { this.onSaved, this.validator, this.initialValue, + this.autovalidate: false, }) : super(key: key) { assert(builder != null); } @@ -181,6 +195,12 @@ class FormField 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 createState() => new FormFieldState(); } @@ -194,8 +214,9 @@ class FormFieldState extends State> { /// 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 extends State> { }); } + /// 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 extends State> { @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); } diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index ca0ba77f58d..1c0fd1af6c2 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -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 formKey = new GlobalKey(); 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 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); }); }