Flutter - Checking Validation of TextFormField - flutter

Hi im new to flutter.
Im using TextFormField widget to validate the input if the textformfield is empty and wrapping it with GlobalKey Form widget.
Can i ask if its possible just only check atleast one field is not empty then its valid.
TextField A = empty & TextField B = not empty :: valid
TextField A = not empty & TextField B = empty :: valid
TextField A = empty & TextField B = empty :: not valid
This is the situation there are two textformfield A and textformfield B atleast one must not be empty so it could be A or B. But if both is empty then the user must fill one textfield. My objective is all my textformfield has a validation but its okay if atleast one is filled or not empty.
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(children: [
TextFormField(validator: (value) {
if (value.isEmpty) {
return "Please Fill";
}
}),
TextFormField(validator: (value) {
if (value.isEmpty) {
return "Please Fill";
}
}),
RaisedButton(
child: Text("Submit"),
onPressed: () async {
if (_formKey.currentState.validate()) {
return;
}
_formKey.currentState.save();
//Some Codes
},
)
]),
),
I was planing to change it to TextField widget and use setState({}) to check if atleast 1 got filled but i dont want to use setState. Is there a way to solve my problem?. Thanks

Try this:
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(children: [
TextFormField(validator: (value) {
if (value.isEmpty) {
return "Please Fill";
}
return null;
}),
TextFormField(validator: (value) {
if (value.isEmpty) {
return "Please Fill";
}
return null;
}),
RaisedButton(
child: Text("Submit"),
onPressed: () async {
if (_formKey.currentState.validate()) {
// If the form is valid, display a Snackbar.
Scaffold.of(context).showSnackBar(SnackBar(content: Text('Processing Data')));
}
_formKey.currentState.save();
//Some Codes
},
)
]),
),

Related

Validation event for custom widget

In flutter, a TextFormField for an example, has a validator which can be used for validation:
TextFormField(validator: validator ...
Then in your form, you can call validate on the Form widget to make all children with these validators validate:
_formKey.currentState!.validate()
If a validation fails, the TextFormField will display an error text along with a position and color transition.
I have my own custom photo widget, and I would like to make it able to support the same validation functionality. That is, give it a validator, hook it up to the validate() event, and if the user hasn´t added any photo, the validation fails and shows the error text the validator returns.
But I cannot figure out how to implement the validate eventlistener on a custom widget. So how would you go around this?
Update:
#user18309290 pointed me in the direction of extending my widget from FormField. But the problem is that my widget has internal functions and properties I need to access in the instance/layout tree. But I can´t figure out the right way to do it. I could put all the stuff in the build method, but that means that all of my "heavy" logic and properties would be reinstantiated every time the widget rebuilds if I understand correctly. So how do I extend from FormField to have validation support (validation fails if image list is empty), but still have access to my methods and properties?
This is my simplified widget:
class MyPhotoComponent extends FormField<List<File>> {
late String title;
List<File> images = [];
openCamera() {
print('This and other methods, has alot of logic');
images.add(File('filepath'));
}
String internalTitle = 'Internal title';
MyPhotoComponent({required String title, required FormFieldSetter<List<File>> onSaved, FormFieldValidator<List<File>>? validator, required List<File> initialValue, Key? key})
: super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
key: key,
builder: (FormFieldState<List<File>> state) {
return Column(
children: [
Builder(builder: (BuildContext context) {
return Column(
children: [
Text(internalTitle), //Error: The instance member 'internalTitle' can't be accessed in an initializer.
MyOtherPhotoGalleryComponent(images: images), //Error: The instance member 'images' can't be accessed in an initializer.
ElevatedButton.icon(
onPressed: openCamera, //Error: The instance member 'openCamera' can't be accessed in an initializer.
icon: Icon(Icons.add_a_photo),
label: Text('Take photo'),
),
],
);
}),
if (state.hasError) Builder(builder: (BuildContext context) => Text('Validation error'))
],
);
},
);
}
Inherit a custom widget from FormField. Each individual form field should be wrapped in a FormField widget like TextFormField.
Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
CustomFormField(
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please select something';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {}
},
child: const Text('Submit'),
),
],
),
);
There are three ways to accomplish this
State management which will be complicated for this scenario
SetState() which will update your whole UI and will be expensive
ValueNotifier and ValueListableBuilder which I recommend
First define a valuenotifier
late ValueNotifier<bool> _isValid;
Then in initState initialize it and add a listener to it which will be your validator
...
_isValid = ValueNotifier(true);
_isValid.addListener((){
If(){ //your validation
_isValid.value = true;
} else {
_isValid.value = false;
}
})
Then in your UI add ValueListableBuilder and put your widget inside it to listen to its changes and change accordingly
ValueListableBuilder(
listenableValue: _isValid,
builder: (context, bool yourValue, child){
// return your widget and use yourValue to update your UI
}
)
Sorry if there is any misspelling. I wrote it on my mobile

how to rebuild dialog Widget on changing bool variable state?

im trying to submit form on Dialog and i have a DateTimePicker button and need to make a validation on it also before submitting , what i want to do is showing a text error in case no date picked by changing my own variable "isValid" to false but the UI is not updating , i have to close the dialog and reopen it to see the error text even though i wrapped my column with a StatefulBuilder
my dialog photo here
here is my code
StatefulBuilder(builder: (context, StateSetter setState) {
return isValid == false
? Column(
children: [
ElevatedButton(
onPressed: () {
DateTimePicker(context)
.then((value) => setState(() {
_appointmentDateTime = value;
}));
},
child: Text(getTimeDate())),
Text(
'error',
style: TextStyle(
color: Colors.red, fontSize: 10),
),
],
)
: Column(
children: [
ElevatedButton(
onPressed: () {
DateTimePicker(context)
.then((value) => setState(() {
_appointmentDateTime = value;
}));
},
child: Text(getTimeDate())),
],
);
})
Validating form + toggling the isValid Value is working fine
OutlinedButton(
onPressed: () async {
if (_formKey.currentState.validate() &&
_appointmentDateTime != null) {
String date = DateFormat('yyyy-MM-dd hh:mm')
.format(_appointmentDateTime);
var appointment = Appointment(
patientName: widget.patient.name,
date: date,
hospital: _hospitalController.text,
await FirebaseApi.addPatientAppointment(
widget.patient.id, appointment);
print('Appointment Created ');
_formKey.currentState.reset();
setState(() {
translator = null;
_appointmentDateTime = null;
});
Navigator.pop(context);
}
else {
setState(() {
isValid = !isValid;
});
}
},
child: Text('Add Appointment')),
It can get confusing when writing the code like this when dealing with Dialogs. The setState you are using in the OutlinedButton is not the same as the setState used in the StatefulBuilder. You need to enclose your OutlinedButton inside the StatefulBuilder too. If you ever use a StatefulBuilder inside a stateful widget, it is better to use a different name like e.g setDialogState.
It is even better to create a separate stateful widget class just for your Dialog contents and pass the formKey and anything else than using a StatefulBuilder in this case to avoid confusion.

How to do integration test with flutter driver on autocomplete textfield in flutter

How to do integration test with flutter driver on AutoTextCompleteFiled with GlobalKey value. How we can identify the AutoTextCompleteFiled widget with the Global key and how we can enter text in this widget with flutter driver automation in BDD?
Here's how you can do it I have given example of flutter_typeahead which is also used for autocomplete
This is the test case
group('App', () {
final saveButton = find.byValueKey('save_button');
final cityField = find.byValueKey('city_field');
final city = 'Mumbai';
FlutterDriver driver;
// Connect to the Flutter driver before running any tests.
setUpAll(() async {
driver = await FlutterDriver.connect();
});
// Close the connection to the driver after the tests have completed.
tearDownAll(() async {
driver.close();
});
test('test cities field', () async {
//for tapping city autocomplete field
await driver.tap(cityField);
//for entering city
await driver.enterText(city);
//for selecting suggestion or find.text(city) to search by text
await driver.tap(find.byValueKey(city));
//for tapping save button
await driver.tap(saveButton);
});
});
This is the autocomplete field
Main things to note here is that you need to use key in each item builder widget that will be used later to tap that widget
You can also use find.text(city) for searching by text instead of key
city name used for testing must be present in the cities list used
Column(
children: <Widget>[
Text('What is your favorite city?'),
TypeAheadFormField(
key: Key('city_field'),
textFieldConfiguration: TextFieldConfiguration(
controller: this._typeAheadController,
decoration: InputDecoration(labelText: 'City')),
suggestionsCallback: (pattern) {
return getSuggestions(pattern, citiesList);
},
itemBuilder: (context, suggestion) {
return ListTile(
key: Key(suggestion),
title: Text(suggestion),
);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (suggestion) {
this._typeAheadController.text = suggestion;
},
validator: (value) {
if (value.isEmpty) {
return 'Please select a city';
}
return null;
},
onSaved: (value) => this._selectedCity = value,
),
TextButton(
key: Key('save_button'),
onPressed: () {},
child: Text('Save'),
),
],
)
Cities List
List<String> citiesList = [
'Mumbai',
'Pune',
'Delhi',
'Surat',
'Jaipur',
'Chennai',
'Kolkata',
'Bangalore'
];

Flutter TextField calls onSubmitted unexpectedly

My case is I have a widget with TextField used for search. When I type something in TextField the cross icon become visible in suffixIcon (to clear). I do not do search and just click the cross icon to clear the entered input but onSubmitted is called and search executed!!! But I don't need it! I do not submit the text input, I cancel it!!
final searchClear = ValueNotifier(false);
final searchController = TextEditingController();
// in initState method:
searchController.addListener(() {
searchClear.value = searchController.text.isNotEmpty;
});
// in build method:
TextField(
...
controller: searchController,
suffixIcon: ValueListenableBuilder<bool>(
valueListenable: searchClear,
builder: (_,visible,child) {
return Visibility(
visible: visible,
child:child,
);
},
child: InkWell(
child: Icon(Icons.close),
onTap: () {
searchController.clear();
searchFocus.unfocus();
}
),
),
onSubmitted: (value) {
if(value.isEmpty) {
FocusScope.of(context).requestFocus(searchFocus);
} else {
widget.search(value);
}
}
),
P.S. Any ideas to work around this?

How to reset error message in TextFormField?

I know this is a simple question but i did not find how to do this. How can i reset error message in TextFormField when the user input something in the field. Something like onchange listener. I notice onChanged is available in TextField but not TextFormField. How can i do this?
final _email = Container(
child: TextFormField(
decoration: InputDecoration(labelText: email),
keyboardType: TextInputType.emailAddress,
controller: emailController,
validator: _validateEmail,
onSaved: (input) => _stringEmail = input.toLowerCase().trim(),
),
);
UPDATE: I am using controller to add listener for user input. But i just want to reset the error message instead of validating the form again. Is this possible? How can i do this?
_resetEmailErrorMessage() {
print("Second text field: ${emailController.text}");
//replace clear to something that can reset the validation error message
emailController.clear();
}
#override
void initState() {
super.initState();
//start listening to changes
emailController.addListener(_resetEmailErrorMessage);
}
You can use auto-validation feature of Form
If you looking for this solution,
"As soon as you type it validates your input and show/hide error"
Flutter provides auto-validation feature, you just need to enable it at form level.
Default
_autoValidate = false;
Form
body: Form( key: _formKey,
autovalidate: _autoValidate,....)
Enable it when user presses submit once,
if (_formKey.currentState.validate()) {
// All Good
} else {
// start auto validate
setState(() {
_autoValidate = true;
});
}
Update :-
Now Form.autovalidate is deprecated. So Use
Form.autovalidateMode = AutovalidateMode.onUserInteraction
Form.autovalidate has been deprecated. Use Form.autovalidateMode = AutovalidateMode.onUserInteraction to have the errors automatically clear once a user begins to interact with the form field again without having to manage any state yourself.
The TextFormField also has the option of onChanged in that you can try to reset the form validation through the FormKey
final _email = Container(
child: TextFormField(
decoration: InputDecoration(labelText: email),
keyboardType: TextInputType.emailAddress,
controller: emailController,
validator: _validateEmail,
onChanged: (value){
formKey.currentState.reset();
},
onSaved: (input) => _stringEmail =
input.toLowerCase().trim(),
),
);
// Somewhere in `State` declaration
final formKey = GlobalKey<FormState>();
bool autovalidate = false;
#override
void build(BuildContext context) {
return Form(
key: formKey,
autovalidate: autovalidate,
child: Column(
children: [
TextFormField(
...
validator: (value) {
return (value == null || value.isEmpty) ? 'Cannot be empty' : null;
}
onChanged: (value) {
// Call this to refresh autovalidation result
setState(() {});
}
),
],
),
);
}
void validate() {
if(formKey.currentState.validate()) {
// data is valid!
return true;
} elsee {
// Activate autovalidation
autovalidate = true;
}
return false;
}
I would suggest to use the controller and listen to the User input . and whenever he types something you can validate() your form again
I don't see any answers to your question. I also ran into a situation like this I wanted the fields of the authentication form to reset their validation error once the user changes to Login or Signup. So I added this logic:
class _AuthFormState extends State<AuthForm> {
...
var _formFlipped = false; // THIS RIGHT HERE
void _trySubmit() {
final isValid = _formKey.currentState.validate();
FocusScope.of(context).unfocus();
...
}
#override
Widget build(BuildContext context) {
return
...
child: Form(
...
TextFormField(
key: ValueKey('email'),
validator: (value) {
if (_formFlipped) return null; // THIS
if (value.isEmpty || !value.contains('#')) {
return 'Enter a valid email';
}
return null;
},
...
RaisedButton(
child: Text(_isLogin ? 'Login' : 'Signup'),
onPressed: () {
setState(() {
_formFlipped = false; // AND THIS
});
_trySubmit();
},
),
FlatButton(
child: Text(_isLogin
? 'Create new account'
: 'I already have an account'),
onPressed: () { // THIS TOO
setState(() {
_isLogin = !_isLogin;
_formFlipped = true;
_formKey.currentState.validate();
});
},
),
...
Before & After.
Edit:
So basically you just force pass null on all your field validators to clear the message. Alternatively as Yogesh pointed out you can achieve this by simply _formKey.currentState.reset();
But if you need more granular control, this method serves you good since you can set if (_formFlipped) return null; or similar condition to different fields according to your need.
This one worked form. You can try to validate form like this:
_formKey.currentState.validate()
You need to define validate field in your TextFormField.