Validation event for custom widget - flutter

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

Related

Showing a button to the user based on the data entered in the TextFormField

final TextEditingController _weight = TextEditingController();
if (_weight.text.contains(RegExp(r'[0-9]')))
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: BMIButton(
onpressed: () {
Navigator.push(
context,
PageTransition(
type: PageTransitionType.rightToLeft,
child: BMIHeight(),
inheritTheme: true,
ctx: context),
);
},
))
I'm trying to show an OutlinedButton when the user enters some data into the textFormField. When I enter a value in the TextFormField and confirm, it doesn't show a button, but when I hot reload it, it sees that value and shows the button.
Try to add listener and call setState to update ui
late final TextEditingController _weight = TextEditingController()
..addListener(() {
setState(() {});
});
And override the dispose method and depose this controller.
This means you need just to update the state of your widget, first make sure this inside a StatefulWidget, then add a SetState(() {}) on the end of that method:
if (_weight.text.contains(RegExp(r'[0-9]')))
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: BMIButton(
onpressed: () {
Navigator.push(
context,
PageTransition(
type: PageTransitionType.rightToLeft,
child: BMIHeight(),
inheritTheme: true,
ctx: context),
);
},
))
//...
setState(() {}) // add this
TextEditingController is implementing Listenable. You can exploit that to build part of your UI conditionally.
Moreover, TextEditingController must be correctly initialized and disposed of. You can do that smoothly with flutter_hooks.
You'd obtain this concise result:
class MyWidget extends HookWidget {
const MyWidget({super.key});
#override
Widget build(BuildContext context) {
final textController = useTextEditingController();
return Column( // example, I'm not sure of what you've got there
children: [
TextField(controller: textController), // Your Text Field
ValueListenableBuilder(
valueListenable: textController,
builder: (context, value, child) {
return value.text.contains(...)
? OutlinedButton( // Your Button
onPressed: () {
// .. do stuff
},
child: const Text('I am ready to be pressed'),
)
: const SizedBox.shrink(); // This is empty, but you can render whatever
},
)
],
);
}
}
Your code that checks if the TextFormField is populated needs to be inside the build function of a stateful widget.
To trigger the state update, listen to changes on the TextFormField, inside the function set the state of some variable that you can check.
Add this to the initState method:
_weight.addListener(() {
setState(() {
_inputText = _weight.text; // Create this variable inside the state class
});
});
And change your if statement as follows:
if (_inputText.contains(RegExp(r'[0-9]')))
Method 2
You could also Wrap your Widget with a Visibility Widget:
Visibility(
visible: RegExp(r'[0-9]').hasMatch(_inputText),
child: [YOUR WIDGET],
)
This still needs the listener with the setState call.

FORMS, no further idea how to implement a TextField with a CupertinoApp

This drives me nuts. I am trying to get a CupertinoTextField into a Form. But no matter what I try, everything turns out as a dead end.
The complication of my case is, the form in embedded into the Flushbar package, which provides kind of a snackbar to my CupertinoApp. And this is a stateless widget ;-(
return Flushbar(
backgroundColor: Common.inputBlue,
userInputForm : Form(
key: _formKey,
child: Column(
children: <Widget> [
_DelayStatusFormField(
initialValue: task.delayStatus,
onSaved: (value) => otherFormDataSubmit.delayStatus = value,
//onSaved: (value) => this.delayStatus = value,
),
CupertinoTextField(
controller: _delayTec,
onChanged: (text) {
state.didChange(text);
},
),
]),
),
);
In the onChanged section I intend to implement some validation, so I need kind of a rebuild. But setState obviously doesn't work without a proper state.
Using TextFormField also looks kind of dodgy, because I do not only have to embed this in Material( but in loads of further localization Widgets.
The most desirable solution, is this one where I tried several variants: embed the CupertinoTextField in a FormField class
class _DelayFormField extends FormField<String> {
_DelayFormField({
FormFieldSetter<String> onSaved,
String initialValue,
bool enabled,
GlobalKey key,
}) : super(
onSaved: onSaved,
initialValue: initialValue,
key: key,
builder: (FormFieldState<String> state) {
//TextEditingController inputTec = TextEditingController(text: initialValue);
return Column(children: [
SizedBox(
width: 100, height: 30,
child: CupertinoTextField(
//controller: inputTec,
enabled: true,
onChanged: (text) {
initialValue = text;
state.didChange(text);
},
)),
]);
}
);
}
The problem here, I the TextEditingController is reinitialized on any rebuild with initialVale, loosing my input. I have got around this by assigning the input to initialVale, but the cursor is then always set leading to the prefilled digits. So you type the number backwards. Since everything is in the class' initialization, I cannot find a spot where to initialize the TextEditingController outside of the build method.
Any other idea on how to solve this? Getting such a class working would be great, since one could then simply re-use this class, frequently.

How to validate a text field on value changed?

How to autovalidate a text form field when its value changes?
i tried
bool _autoValidate = false;
TextFormField(
autovalidate: _autoValidate ,
onChanged: (value) {
setState(() {
_autoValidate = true;
});
},
validator: (value) {
if (value.length < 5) {
return 'False';
} else {
return 'True';
}
},
),
But not working, TextFormField still doesn't show errors on validation.
I need a way to turn on the validation on text changed.
Flutter has nows an API to validate a form field only when the content changes. You just need to use the autovalidateMode parameter and set it to AutovalidateMode.onUserInteraction.
The following text field will only validate when the user changes its content:
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
),
),
);
}
}
See AutovalidateMode docs for more options in when to validate.
This API is now available on the latest stable channel. Let me know if it solves your problem.
I think,
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
)
is the best way to solve this issue.

Flutter - Checking Validation of TextFormField

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
},
)
]),
),

How to open DropdownButton when other widget is tapped, in Flutter?

I need to have a DropdownButton's list of options open/show programmatically when some other widget is tapped. I know that this may not be UI-best-practice and all, but I need this behavior:
As an example, in a structure like the one below, I may need to have taping Text("every") to open the neighboring DropdownButton's dropdown list, behaviors similar to clicking a <select>'s label in HTML.
Row(children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('every'),
),
Expanded(
child: DropdownButton<String>(
value: _data['every'],
onChanged: (String val) => setState(() => _data['every'] = val),
items: _every_options.map<DropdownMenuItem<String>>(
(String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
},
).toList(),
isExpanded: true,
),
),
]);
NOTE: I am in need though of the general solution to this problem, not just how to make that Text behave somewhat "like a HTML label" in the tree below. It may need to be triggered to open by maybe a further away button etc.
The other answer is the best way to do this, but as requested by the OP in comments, here are two very "hacky" ways to achieve this, yet without implementing custom widgets.
1. Access DropdownButton widget tree directly using GlobalKey
If we look at the source code of DropdownButton, we can notice that it uses GestureDetector to handle taps. However, it's not a direct descendant of DropdownButton, and we cannot depend on tree structure of other widgets, so the only reasonably stable way to find the detector is to do the search recursively.
One example is worth a thousand explanations:
class DemoDropdown extends StatefulWidget {
#override
InputDropdownState createState() => DemoDropdownState();
}
class DemoDropdownState<T> extends State<DemoDropdown> {
/// This is the global key, which will be used to traverse [DropdownButton]s widget tree
GlobalKey _dropdownButtonKey;
void openDropdown() {
GestureDetector detector;
void searchForGestureDetector(BuildContext element) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is GestureDetector) {
detector = element.widget;
return false;
} else {
searchForGestureDetector(element);
}
return true;
});
}
searchForGestureDetector(_dropdownButtonKey.currentContext);
assert(detector != null);
detector.onTap();
}
#override
Widget build(BuildContext context) {
final dropdown = DropdownButton<int>(
key: _dropdownButtonKey,
items: [
DropdownMenuItem(value: 1, child: Text('1')),
DropdownMenuItem(value: 2, child: Text('2')),
DropdownMenuItem(value: 3, child: Text('3')),
],
onChanged: (int value) {},
);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Offstage(child: dropdown),
FlatButton(onPressed: openDropdown, child: Text('CLICK ME')),
],
);
}
}
2. Use Actions.invoke
One of the recent features of Flutter is Actions (I'm not sure what it's meant for, I've only noticed it today after flutter upgrade), and DropdownButton uses it for reacting to different... well, actions.
So a little tiny bit less hacky way to trigger the button would be to find the context of Actions widget and invoke the necessary action.
There are two advantages of this approach: firstly, Actions widget is a bit higher in the tree, so traversing that tree wouldn't be as long as with GestureDetector, and secondly, Actions seems to be a more generic mechanism than gesture detection, so it's less likely to disappear from DropdownButton in the future.
// The rest of the code is the same
void openDropdown() {
_dropdownButtonKey.currentContext.visitChildElements((element) {
if (element.widget != null && element.widget is Semantics) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is Actions) {
element.visitChildElements((element) {
Actions.invoke(element, Intent(ActivateAction.key));
return false;
});
}
});
}
});
}
It's one (of many) designed API limitations...
The easiest approach to accomplish what you want, without modifying the SDK, copy dropdown.dart, and create your own version of it, let's say custom_dropdown.dart, and paste the code there ...
in line 546, rename the class to CustomDropdownButton, and in line 660 and 663 rename _DropdownButtonState to CustomDropdownButtonState, ( we need the state class to be exposed outside the file ).
Now you can do whatever you want with it,
although you were interested in the _handleTap(), to open the overlay menu options.
Instead of making _handleTap() public, and refactor the code, add another method like:
(line 726)
void callTap() => _handleTap();
Now, change your code to use your DropdownButton instead of the Flutter's DropdownButton, the key is to "set the key" (Global one) :P
// some stateful widget implementation.
Map<String, String> _data;
List<String> _every_options;
// we need the globalKey to access the State.
final GlobalKey dropdownKey = GlobalKey();
#override
void initState() {
_every_options = List.generate(10, (i) => "item $i");
_data = {'every': _every_options.first};
simulateClick();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Row(children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('every'),
),
Expanded(
child: CustomDropdownButton<String>(
key: dropdownKey,
value: _data['every'],
onChanged: (String val) => setState(() => _data['every'] = val),
items: _every_options
.map((str) => DropdownMenuItem(
value: str,
child: Text(str),
))
.toList(),
isExpanded: true,
),
),
]),
);
}
void simulateClick() {
Timer(Duration(seconds: 2), () {
// here's the "magic" to retrieve the state... not very elegant, but works.
CustomDropdownButtonState state = dropdownKey.currentState;
state.callTap();
});
}