I have a Flutter Form with many dropdownformfield and textformfield widgets, validating these is trivial using the validate: method. Validation for visual fields is obvious.
However, in many forms a non-visible element may need to be validated. For instance, if taking a picture in a Form, there will only be a button to take the picture which will input the resulting filename into a String var. I would need to validate the String var in this case and return the validation result to the button (i.e. display a "Required field" below the button), but of course the String var is not held in any formfield widget.
This being said, how can I either "wrap" the button in a widget which contains a validator: method, or how can I add a validator to the button itself AND then display the appropriate validation message to the user in the UI?
Thank you!
You could create your own FormField:
class TakePictureFormField extends FormField<String> {
/// Creates a [FormField] that contains an [ElevatedButton] to take a picture
/// with the phone camera.
///
/// The [String] value corresponds to the path of the picture taken.
TakePictureFormField({
Key? key,
String? initialValue,
FormFieldSetter<String>? onSaved,
FormFieldValidator<String>? validator,
bool enabled = true,
AutovalidateMode? autovalidateMode,
ButtonStyle? buttonStyle,
void Function(String)? onChanged,
}) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: validator,
enabled: enabled,
autovalidateMode: autovalidateMode,
builder: (FormFieldState<String> field) {
final currentValue = field.value;
return InputDecorator(
decoration: InputDecoration(
border: InputBorder.none,
errorText: field.errorText,
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(field.context).errorColor,
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
style: buttonStyle,
onPressed: () async {
// Fake implementation to take a picture.
final value = await Future<String>.delayed(
const Duration(microseconds: 300),
() => 'my_path/to/image');
field.didChange(value);
if (onChanged != null) {
onChanged(value);
}
},
child: const Text('Take a Picture'),
),
if (currentValue != null) Text(currentValue),
],
),
);
},
);
}
And then use it inside a Form like you would for any other FormField widget:
TakePictureFormField(
validator: (val) =>
val == null || val.isEmpty ? 'Error invalid picture' : null,
)
Try the complete example on DartPad
Related
I've created a custom form field and in it is a GestureDetector but the onTap is not firing. I want to use the GestureDetector to collect the click event when someone clicks on the form field in a disabled state so I can launch a dialog box. Code is below. I'm wondering what I must be doing wrong or misunderstanding.
Widget _customFormField(
{required String title,
required String initialValue,
required int maxLines,
required int maxLength,
required bool enabled,
required TextEditingController controller,
bool autoFocus = false,
required FocusNode currentFocusNode,
required FocusNode futureFocusNode,
Function? function,
bool formEnd = false}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
fontFamily: 'Ariel',
color: Colors.pink)),
GestureDetector(
onTap: () {
print('onTap'); // <-------------------- This never gets hit
function;
},
child: TextFormField(
autofocus: autoFocus,
enabled: enabled,
textInputAction:
formEnd ? TextInputAction.done : TextInputAction.next,
onEditingComplete: () =>
FocusScope.of(context).requestFocus(futureFocusNode),
focusNode: currentFocusNode,
controller: controller,
keyboardType: TextInputType.text,
validator: (value) {
if (value!.isEmpty) {
if (_formValid) {
_formValid = false;
currentFocusNode.requestFocus();
}
//title.toLowerCase()
return 'Please enter the ${title.toLowerCase()}';
} else {
return null;
}
},
),
),
],
);
}
I've tried changing it to an InkWell but it behaves the same.
I've removed some styling code from the example but nothing that could have an affect on the problem.
well the function property that you got from the properties are the definition of the method, when you do :
onTap: () {
print('onTap');
function;
},
you not calling the method, you putting it there, you need to call it so it runs, by adding () or by calling call() on it.
onTap: () {
print('onTap');
function(); // like this
// function.call(); or like this
},
Try the following:
onTap: () {
print('onTap'); // <-------------------- This never gets hit
if (function != null) function.call();
}
My setup is simple: i am making a landing page using Flutter web: users input their email add and tick a Checkbox if say they are over 18.
For the life of me I can not find a way to store both email add and the boolean value of the checkbox in the SAME RECORD in Firebase?
the UI and Firebase all setup and working ok, here the code snippets:
`child: TextFormField(
controller: _emailController,
// The validator receives the text that the user has entered.
validator: (val) => !EmailValidator.validate(val!, true)
? 'Please enter a valid email.'
: null,
onSaved: (email2save) => this.email2save = email2save,
decoration: InputDecoration(
icon: Icon(
Icons.email_outlined,
color: Color(0xFF0000CC),
),
hintText: "Please enter your email",
border: InputBorder.none),
),
.......
Column(
children: [
Text('Please tick here'),
MyStatefulWidget(),
Text(' if over 18'),
],
),
.......
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool isChecked = false;
#override
Widget build(BuildContext context) {
Color getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Color(0xFF0000CC);
}
return Colors.black;
}
return Checkbox(
checkColor: Colors.white,
fillColor: MaterialStateProperty.resolveWith(getColor),
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
);
}
Hi I don't see any Firebase in the code you've provided above but it is actually pretty easy to add two or even many things at once to Cloud Firestore:
var collection = FirebaseFirestore.instance
.collection(collection_name)
.doc(document_name);
collection
.set({
'email': 'email2save',
'check': 'isChecked'
})
You can do this anywhere in your class and it will work like magic.
I'm using the intl_phone_field package to verify phone numbers based on country code.
The IntlPhoneField checks automatically for invalid phone numbers, and prompt a message respectively.
It support onChange, onSubmit and onSaved functions, but does not support an onValid function.
I want to enable/disable a Submit Button, based on package's validate function, Since it already supports many country codes and different phone number lengths.
Widget build(BuildContext context) {
return IntlPhoneField(
autofocus: true,
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
counterText: '',
),
initialCountryCode: 'US',
// countries: const ['US'],
);
}
How do I achieve that?
Solved!
I detected validation using the intl_phone_field package by exporting the Country object, which contains the minLength and maxLength of the country code phone number.
Using those parameters I verified the phone number in the onChange function and a function in case of verified phone number.
class PhonePicker extends StatelessWidget {
const PhonePicker({super.key});
#override
Widget build(BuildContext context) {
const _initialCountryCode = 'US';
var _country =
countries.firstWhere((element) => element.code == _initialCountryCode);
return IntlPhoneField(
autofocus: true,
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
counterText: '',
),
initialCountryCode: _initialCountryCode,
onChanged: (value) {
if (value.number.length >= _country.minLength &&
value.number.length <= _country.maxLength) {
// Run anything here
}
},
onCountryChanged: (country) => _country = country,
);
}
}
I have opened a pull request with a solution in the GitHub project.
You can a state variable and use validator data to check whether number is valid or not
bool isValid = false;
#override
Widget build(BuildContext context) {
return Column(
children: [
IntlPhoneField(
autofocus: true,
validator: (p0) {
/// logic to validate number
/// isValid =true
},
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
counterText: '',
),
initialCountryCode: 'US',
// countries: const ['US'],
),
ElevatedButton(onPressed: isValid ? () {} : null, child: child)
],
);
Also you can use TextEditingController with listener and check data and enable button state. make sure to use setState.
I need separate logic with UI.
I used the following example:
1.- Use a class validation item to show a string value and error.
class ValidationItem {
final String value;
final String error;
ValidationItem(this.value, this.error);
}
2.- Use the next code for provider class.
class SignupValidation with ChangeNotifier {
ValidationItem _firstName = ValidationItem(null,null);
//Getters
ValidationItem get firstName => _firstName;
bool get isValid {
if (_firstName.value != null){
return true;
} else {
return false;
}
}
//Setters
void changeFirstName(String value){
if (value.length >= 3){
_firstName=ValidationItem(value,null);
} else {
_firstName=ValidationItem(null, "Must be at least 3 characters");
}
notifyListeners();
}
void submitData(){
print("FirstName: ${firstName.value}");
}
}
3.- Use the next widget to show text field and validate
class Signup extends StatelessWidget {
#override
Widget build(BuildContext context) {
final validationService = Provider.of<SignupValidation>(context);
return Scaffold(
appBar: AppBar(
title: Text('Signup'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: <Widget>[
TextField(
decoration: InputDecoration(
labelText: "First Name",
errorText: validationService.firstName.error,
),
onChanged: (String value) {
validationService.changeFirstName(value);
},
),
RaisedButton(
child: Text('Submit'),
onPressed: (!validationService.isValid) ? null : validationService.submitData,
)
],
),
),
);
}
}
The problem is the performance for example every time the text is changed the notifyListener() Is calles.
My question: Is there a cost to performance?
you can use TextFormField instead of TextField.
Best way to validate fields is that you can use validator property TextFormField property as bellow
TextFormField(
decoration: InputDecoration(
labelText: "First Name",
errorText: validationService.firstName.error,
),
validator: (e){
if(e!.trim().isEmpty) return "String is empty";
return null;
},
onChanged: (String value) {
validationService.changeFirstName(value);
},
),
The TextField itself gives you the ability to validate the form, then why to make it complex by implementing notifier, instead you want to make it common you can make the validater global function for it. Nad user it in the validate function.
Void validateEmail(String value){ // your logic}
Use this function as follow
TextFormField(
decoration: InputDecoration(
labelText: "First Name",
errorText: validationService.firstName.error,
),
validator: validateEmail(),
onChanged: (String value) {
validationService.changeFirstName(value);
},
Secondly to get the value of inputted string you have a TextEditingController which directly give you the string you inputted.
Declare TextEditingController as follow
TextEditingController emailCont = TextEditingController();
Use this controller in TextField as follow
TextFormField(
controller: emailCont,
decoration: InputDecoration(
labelText: "First Name",
errorText: validationService.firstName.error,
),
validator: validateEmail(),
onChanged: (String value) {
validationService.changeFirstName(value);
},
Now to get the value from this controller you can get it this way.
emailCont.text
This way it will be easy to manage and less complexity.
maybe I am overstressing this package, but the last example shows a form. However, my implementation shows no sign of life. Not even an error or warning.
My approach is a little different. I want to create a form with multiple, different input widgets (text, button, selector) and then return those with a bloc event. The returning/saving part is not yet implemented.
If someone has implemented a form with flushbar, I would appreciate any advice.
This is my flushbar code
Widget otherActionsSlider() {
bool paused;
int priority;
bool supply;
return Flushbar(
userInputForm : Form(
key: _formKey,
child: Container(
child: Column(
children: <Widget> [
Text("progress"),
_DelayStatusFormField(
onSaved: (value) => this.delayStatus = value,
),
Row(
children: [
_DelayFormField(
onSaved: (value) => this.delay = value,
enabled: this.delayStatus == DelayStatus.unrecoverable ? true : false
)
],
),
RaisedButton(
child: Text('Submit'),
onPressed: () {
if (this._formKey.currentState.validate()) {
setState(() {
this._formKey.currentState.save();
});
}
},
),
],),)),
);
}
and the form field classes
class _DelayStatusFormField extends FormField<DelayStatus> {
_DelayStatusFormField({
FormFieldSetter<DelayStatus> onSaved,
FormFieldValidator<DelayStatus> validator,
DelayStatus initialValue,
}) : super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<DelayStatus> state) {
return
CupertinoSlidingSegmentedControl(
groupValue: initialValue == DelayStatus.recoverable ? 1 :
initialValue == DelayStatus.unrecoverable ? 2 : 0,
children: {
0: Text('on time'),
1: Text('recoverable'),
2: Text('unrecoverable')
},
onValueChanged: (int val) => state.didChange(val == 0 ? DelayStatus.onTime :
val == 1 ? DelayStatus.recoverable :
DelayStatus.unrecoverable)
);
}
);
}
class _DelayFormField extends FormField<int> {
_DelayFormField({
FormFieldSetter<int> onSaved,
FormFieldValidator<int> validator,
int initialValue,
bool enabled
}) : super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
builder: (FormFieldState<int> state) {
TextEditingController inputTec = TextEditingController(text: initialValue.toString());
return
CupertinoTextField(
controller: inputTec,
placeholder: 'delay in days',
enabled: enabled
);
}
);
}
While I haven't yet fully implemented my solution, I finally received some signs of life. My issue was, the function returning the Flushbar has to be of type Flushbar<List<String>> given the example of the package readme. I returned Widget instead.
If someone tries to work with this example, there quite a few brackets and more importantly, it is not (as written in the readme) Flushbar(userInputForm = Form( but Flushbar(userInputForm: Form(
Though the quality of the example given leaves room for improvement, the widget itself is quite nice, especially when implementing more simple use cases (I have a CupertinoApp, so I cannot use snackbar)