Form validation in flutter passes despite not meeting conditions - flutter

I am building a Flutter application with multiple TextFormFields. I create a reusable TextFormfield Widget to keep things modular. The problem is, when the submit button is clicked, even though the Text Form Fields are not valid, it runs like it is valid.
My TextFormField widget:
class AndroidTextField extends StatelessWidget {
final ValueChanged<String> onChanged;
final String Function(String?)? validator;
const AndroidTextField(
{Key? key,
required this.onChanged,
this.validator})
: super(key: key);
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
width: size.width * .9,
child: TextFormField(
validator: validator,
onChanged: onChanged,
);
}
}
How I use it in the Scaffold
AndroidTextField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a valid name';
}
return '';
},
onChanged: (val) {
setState(() {
lastName = val;
});
}),
The form
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
AndroidTextField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a valid name';
}
return '';
},
onChanged: (val) {
setState(() {
firstName = val;
});
}),
TextButton(
child: Text('Press),
onPressed:(){
if (_formKey.currentState!.validate()){
//do something
}else{
//don't do the something
}
}
));
}

I feel a flutter validator should return null if valid, not an empty String.
So the code should be:
AndroidTextField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a valid name';
}
return null;
}
...
Also, try:
final String? Function(String?) validator;
instead of
final String Function(String?)? validator;

Related

Flutter Textformfield validator Focuses Last TextFormfield on validation error instead of first

I've two TextFormFields, it focus on the password field on validation error, even if email field has error already & comes before password field.
Any idea what's going wrong here?
//Email
TextFormField(
controller: _emailController,
focusNode: _emailFocus,
validator: (value) {
String? err = validateEmail(value);
if (err != null) {
_emailFocus.requestFocus();
}
return err;
},
),
//Password
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
validator: (value) {
String? err = validatePassword(value);
if (err != null) {
_passwordFocus.requestFocus();
}
return err;
},
),
String? validateEmail(String? value) {
String pattern = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+#[a-zA-Z0-9](?:[a-zA-Z0-9-]"
r"{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]"
r"{0,253}[a-zA-Z0-9])?)*$";
RegExp regex = RegExp(pattern);
if (value == null || value.isEmpty || !regex.hasMatch(value)) {
return 'Enter a valid email address';
} else {
return null;
}
}
String? validatePassword(String? value) {
String pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$!%*?&])[A-Za-z\d#$!%*?&]{8,}$";
RegExp regex = RegExp(pattern);
if (value == null || value.isEmpty) {
return 'Required';
}
if (value.length < 8) {
return "Length should be 8 or more";
}
if (!regex.hasMatch(value)) {
return "Must contain atleast 1 uppecase, 1 lowercase, 1 special character,";
}
return null;
}
Ignore this silly paragraph:(This is just bunch of text, to tell SO that I have added more question details even if it is NOT required and NOT available)
Wrap it with a form widget and validate it only on a button click like the following.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
const appTitle = 'Form Validation Demo';
return MaterialApp(
title: appTitle,
home: Scaffold(
appBar: AppBar(
title: const Text(appTitle),
),
body: const MyCustomForm(),
),
);
}
}
// Create a Form widget.
class MyCustomForm extends StatefulWidget {
const MyCustomForm({super.key});
#override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
// Create a corresponding State class.
// This class holds data related to the form.
class MyCustomFormState extends State<MyCustomForm> {
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
//
// Note: This is a GlobalKey<FormState>,
// not a GlobalKey<MyCustomFormState>.
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState!.validate()) {
// If the form is valid, display a snackbar. In the real world,
// you'd often call a server or save the information in a database.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
}
},
child: const Text('Submit'),
),
),
],
),
);
}
}
Check this for a detailed explanation
https://docs.flutter.dev/cookbook/forms/validation
Edit
Please remove focus request if it's null. That will always keep the focus on password field if both are null
Problem
You have 2 validators and the last one will work the last. That means if your both TextFormField is not valid the last one always works last and your focus goes to the last one.
Solution
check the other focusNode inside of another and focus password area if email focusNode has not focused like below
//Password
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
validator: (value) {
String? err = validatePassword(value);
if (err != null) {
if(!_emailFocus.hasFocus){
_passwordFocus.requestFocus();
}
}
return err;
},
),

How to change TextFormField initialValue dynamically?

I'm trying to use a value from a provider model to update the initialValue of a TextFormField, but the initialValue doesn't change.
import 'package:flutter/material.dart';
import 'package:new_app/models/button_mode.dart';
import 'package:provider/provider.dart';
class EditWeightTextField extends StatelessWidget {
const EditWeightTextField(
{Key? key})
: super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<ButtonMode>(builder: (context, buttonMode, child) {
return TextFormField(
initialValue: buttonMode.weight.toString(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter weight';
}
return null;
},
);
});
}
}
if instead of a TextFormField I use a Text(${buttonMode.weight}') then the text is updated properly. What can I do to make it work with the TextFormField?
You can use TextEditingController in this case.
TextEditingController _controller = TextEditingController();
Consumer<ButtonMode>(builder: (context, buttonMode, child) {
if(buttonMode.weight != null && _controller.text != buttonMode.weight){ // or check for the null value of button mode.weight alone
_controller.text = buttonMode.weight ?? '' ;
}
return TextFormField(
controller : _controller,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter weight';
}
return null;
},
);
});

How can I use Listview builder inside a Flutter Form widget?

I have a Form widget that has about 30 TextFormField widgets. This is intended to be used as a web app. I'm trying to do the best to have the maximum possible speed. The TextFormField widgets have different text input formatters, controllers and some are required and some are optional. How can I use the ListView.builder inside the Form so the the TextFormField widgets are loaded as needed?
If I could understand, you need to make Form with ListView.builder that will build 30 TextFormField, if that right, you only need to put the ListView.builder inside The Form like this:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
List validators = [
(value) {
if (value == null || value.isEmpty) {
return 'Please enter the number';
}
return null;
},
(value) {
if (value == null || value.isEmpty) {
return 'Please enter the email';
}
return null;
},
(value) {
if (value == null || value.isEmpty) {
return 'Please enter the password';
}
return null;
},
];
List formatters = [
FilteringTextInputFormatter.digitsOnly,
FilteringTextInputFormatter.allow(RegExp('[a-zA-Z]')),
FilteringTextInputFormatter.deny(RegExp(r'[/\\]'))
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
child: ListView.builder(
itemCount: 3,
itemBuilder: (context, index) {
return TextFormField(
validator: validators[index],
inputFormatters: [formatters[index]],
);
},
),
),
floatingActionButton: FloatingActionButton(
child: Text('Click'),
onPressed: () {
if (_formKey.currentState.validate()) {
// If the form is valid, display a snackbar. In the real world,
// you'd often call a server or save the information in a database.
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Processing Data')));
}
},
),
);
}
}
This will check if all the 30 TextFormField are not empty.

Flutter Form Validation with nested Widgets

So for my Registration Screen, I have two TextFormField the user has to fill out: email and password. Because I use the same Styling etc I wanted to refactor the TextFormField into a seperate Widget. In the Main Widget, when the user presses the Register Button, I want to validate all three Fields. I have tried it with a GlobalKey, but I get the Error message "Multiple widgets used the same GlobalKey."
Here is my Registration Screen code:
class RegistrationScreen extends StatefulWidget {
#override
_RegistrationScreenState createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
String email;
String password;
final formKey = GlobalKey<FormState>();
void handleRegisterEmail(){
if (formKey.currentState.validate()){
print('Handling Register');
}
}
void handleEmailChanged(String value) {
setState(() {
email = value;
});
}
void handlePasswordChanged(String value) {
setState(() {
password = value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
AuthTextField(
key: formKey,
type: 'Email',
onChanged: handleEmailChanged,
),
AuthTextField(
key: formKey
type: 'Password',
onChanged: handlePasswordChanged,
),
AuthButton(
text: 'Register',
onPressed: handleRegisterEmail,
),
]
),
);
}
}
And here is the code for the AuthTextField:
class AuthTextField extends StatefulWidget {
final String emailOrPassword;
final Function onChanged;
final GlobalKey<FormState> key; //Here passing the key
AuthTextField({this.onChanged, this.emailOrPassword, this.key})
: super(key: key);
#override
_AuthTextFieldState createState() => _AuthTextFieldState();
}
class _AuthTextFieldState extends State<AuthTextField> {
String validateForm(String value) { //two different validators
if (widget.emailOrPassword == 'Email') {
validateFormEmail(value);
} else {
validateFormPassword(value);
}
}
String validateFormEmail(String value) {
if (!RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String validateFormPassword(String value) {
if (value.length < 6) {
return 'Password must be at least 6 characters.';
}
return null;
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 30.0),
child: TextFormField(
key: widget.key, //Passing the key
validator: validateForm,
keyboardType: (widget.emailOrPassword == 'Email')
? TextInputType.emailAddress
: TextInputType.text,
onChanged: widget.onChanged,
//plus some more styling
),
),
);
}
}
And as I said I get and error message. Any suggestion how to solve this? I tried a bunch of different things (e.g. Wrapping the column in RegistrationScreen in a Form and passing the key only to that form) but nothing work.
The issue here is that you are setting the key of the AuthTextFields instead of storing it in as a separate field on the object. This is setting it as the key for both the AuthTextField and the Form object.
This is what you would want to do instead:
class AuthTextField extends StatefulWidget {
final String emailOrPassword;
final Function onChanged;
final GlobalKey<FormState> formKey; //Here passing the key
AuthTextField({Key key, this.onChanged, this.emailOrPassword, this.formKey})
: super(key: key);
#override
_AuthTextFieldState createState() => _AuthTextFieldState();
}
class _AuthTextFieldState extends State<AuthTextField> {
String validateForm(String value) { //two different validators
if (widget.emailOrPassword == 'Email') {
validateFormEmail(value);
} else {
validateFormPassword(value);
}
}
String validateFormEmail(String value) {
if (!RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String validateFormPassword(String value) {
if (value.length < 6) {
return 'Password must be at least 6 characters.';
}
return null;
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 30.0),
child: TextFormField(
//key: widget.key, //Don't pass the key to the TextFormField only the form
validator: validateForm,
keyboardType: (widget.emailOrPassword == 'Email')
? TextInputType.emailAddress
: TextInputType.text,
onChanged: widget.onChanged,
//plus some more styling
),
),
);
}
}
Then you need wrap your widgets in a Form widget:
class RegistrationScreen extends StatefulWidget {
#override
_RegistrationScreenState createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
String email;
String password;
final formKey = GlobalKey<FormState>();
void handleRegisterEmail(){
if (formKey.currentState.validate()){
print('Handling Register');
}
}
void handleEmailChanged(String value) {
setState(() {
email = value;
});
}
void handlePasswordChanged(String value) {
setState(() {
password = value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: formKey, //Wrap your column in this form widget and set the key
child: Column(
children: <Widget>[
AuthTextField(
key: formKey,
type: 'Email',
onChanged: handleEmailChanged,
),
AuthTextField(
key: formKey
type: 'Password',
onChanged: handlePasswordChanged,
),
AuthButton(
text: 'Register',
onPressed: handleRegisterEmail,
),
]
),
),
);
}
}

Flutter Forms: Get the list of fields in error

Can we get the list of fields in error from Flutter forms after validation ? This will help developers use focus-nodes to redirect the attention to the field in error.
I don't think it is possible to get this kind of information from a Form object or a FormState.
But here is a way around to obtain the result you want (focus on the field in error) :
class _MyWidgetState extends State<MyWidget> {
FocusNode _fieldToFocus;
List<FocusNode> _focusNodes;
final _formKey = GlobalKey<FormState>();
final _numberOfFields = 3;
String _emptyFieldValidator(String val, FocusNode focusNode) {
if (val.isEmpty) {
_fieldToFocus ??= focusNode;
return 'This field cannot be empty';
}
return null;
}
#override
void initState() {
super.initState();
_focusNodes =
List<FocusNode>.generate(_numberOfFields, (index) => FocusNode());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(actions: [
IconButton(
icon: Icon(Icons.check),
onPressed: () {
if (_formKey.currentState.validate()) {
print('Valid form');
} else {
_fieldToFocus?.requestFocus();
_fieldToFocus = null;
}
},
),
]),
body: Form(
key: _formKey,
child: Column(children: [
...List<TextFormField>.generate(
_numberOfFields,
(index) => TextFormField(
decoration: InputDecoration(hintText: "Field $index"),
focusNode: _focusNodes[index],
validator: (val) => _emptyFieldValidator(val, _focusNodes[index]),
),
),
]),
),
);
}
}
You simply need to create a FocusNode for each one of your fields, thanks to that you will be abla to call requestFocus on a precise field (in your case a field considered as invalid). Then in the validator property of your form field, as it is the method called by the FormState.validate(), you need to set a temporary variable which will contains the right FocusNode. In my example I only set the variable _fieldToFocus if it was not already assigned using the ??= operator. After requesting the focus on the node I set _fieldToFocus back to null so it will still works for another validation.
You can try the full test code I have used on DartPad.
Sorry if I have derived a bit from your question but I still hope this will help you.
Expanding on Guillaume's answer, I've wrapped the functionality into a reusable class.
You can view a working example on DartPad here: https://www.dartpad.dev/61c4ccddbf29a343c971ee75e60d1038
import 'package:flutter/material.dart';
class FormValidationManager {
final _fieldStates = Map<String, FormFieldValidationState>();
FocusNode getFocusNodeForField(key) {
_ensureExists(key);
return _fieldStates[key].focusNode;
}
FormFieldValidator<T> wrapValidator<T>(String key, FormFieldValidator<T> validator) {
_ensureExists(key);
return (input) {
final result = validator(input);
_fieldStates[key].hasError = (result?.isNotEmpty ?? false);
return result;
};
}
List<FormFieldValidationState> get erroredFields =>
_fieldStates.entries.where((s) => s.value.hasError).map((s) => s.value).toList();
void _ensureExists(String key) {
_fieldStates[key] ??= FormFieldValidationState(key: key);
}
void dispose() {
_fieldStates.entries.forEach((s) {
s.value.focusNode.dispose();
});
}
}
class FormFieldValidationState {
final String key;
bool hasError;
FocusNode focusNode;
FormFieldValidationState({#required this.key})
: hasError = false,
focusNode = FocusNode();
}
To use it, create your forms as usual, but add a FormValidationManager to your state class, and then use that instance to wrap your validation methods.
Usage:
class _MyWidgetState extends State<MyWidget> {
final _formKey = GlobalKey<FormState>();
final _formValidationManager = FormValidationManager();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field1'),
validator: _formValidationManager.wrapValidator('field1', (value) {
if (value.isEmpty) {
return 'Please enter a value';
}
return null;
})),
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field2'),
validator: _formValidationManager.wrapValidator('field2', (value) {
if (value.isEmpty) {
return 'Please enter a value';
}
return null;
})),
ElevatedButton(
onPressed: () {
if (!_formKey.currentState.validate()) {
_formValidationManager.erroredFields.first.focusNode.requestFocus();
}
},
child: Text('SUBMIT'))
],
),
);
}
#override
void dispose() {
_formValidationManager.dispose();
super.dispose();
}
}