I want to limit the value that user input not to over a expected value in flutter text field.
Example,
If there is a value come from API is 10 and want to limit the input not over 10, if client type 11 or something over 10, want to show alert or make user not to type.
How to control this?
TextFormField(
style: TextStyle(fontSize: 20),
onChanged: (value) {
if (value != "") {
int _checkValue = int.parse(value);
if (_checkValue >
Provider.of<SaleProvider>(context, listen: false)
.remainNewQuantity(
this.currentProductItemSelected.id)) {
return 'error';
} else {
setState(() {
this.qty = int.parse(value);
updateByQty();
});
}
} else {
setState(() {
});
}
},
),
This is my trying, but can't do that I want.
Please check below method. I think this will resolve your issue. If still not work, please let me know
Widget getTextField({required int maxValue}) {
return TextFormField(
controller: _textController,
keyboardType: TextInputType.number,
onChanged: (text) {
if (int.parse(text) > maxValue) {
// show popup here.
_textController.text = validText;
_textController.selection = TextSelection.fromPosition(TextPosition(offset: _textController.text.length));
}else{
validText = text;
}
},
);
}
As an exercise for my own learning I gave it a go and came up with the following approach; creating a bespoke widget which admittedly looks like a lot of code for something so simple... but it appears to work as expected I think and one could modify, expand and integrate it with other elements in various ways.
Usage: const MaxIntField(max: 100),
Implementation:
class MaxIntField extends StatefulWidget {
const MaxIntField({Key? key, this.max = 1}) : super(key: key);
final int max;
#override
State<MaxIntField> createState() => _MaxIntFieldState();
}
class _MaxIntFieldState extends State<MaxIntField> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.value.copyWith(text: '0');
_controller.addListener(() {
if (_controller.text.isNotEmpty && _controller.text != '0') {
int intVal = int.parse(_controller.text);
if (intVal > widget.max) {
setState(() {
_controller.value =
_controller.value.copyWith(text: widget.max.toString());
_showMyDialog();
});
} else if (_controller.text != intVal.toString()) {
//remove leading '0'
setState(() {
_controller.value =
_controller.value.copyWith(text: intVal.toString());
});
}
}
});
}
// assuming using Material
_showMyDialog() async {
showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('AlertDialog Title'),
content: Text('This field is limited to ${widget.max}'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
);
}
}
Related
My Bad
It was problem with my future function HashtagService().getSuggestion('topic'); returning empty List before data is properly loaded. EditTopicPage didn't have any problem.
Original Question
I have text form field inside futurebuilder. When I first open page, future is not loaded. When I click on text field to enter something, future is loaded.
I want future to be loaded when the page is first opened.
class EditTopicPage extends StatefulWidget {
const EditTopicPage({required UserProfileModel userProfile, Key? key}) : _userProfile = userProfile, super(key: key);
final UserProfileModel _userProfile;
#override
_EditTopicPageState createState() => _EditTopicPageState();
}
class _EditTopicPageState extends State<EditTopicPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _controller = TextEditingController();
List<String> _hashList = [];
List<String> _suggestionList = [];
late final Future<List<String>> _future;
bool _disabled = true;
final RegExp hashRegex = RegExp(r'^[a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣|ㆍ|ᆢ]*$');
#override
void initState() {
_future = HashtagService().getSuggestion('topic');
_hashList = widget._userProfile.topic.cast<String>();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
UserProfileService _userProfileService = UserProfileService();
final Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
appBar: TopBar(pageTitle: "관심 대화 주제 수정", context: context),
body: ListView(
padding: EdgeInsets.all(16.sp),
children: [
FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
_suggestionList = List<String>.from(snapshot.data.reversed);
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleLineTextFormField(
controller: _controller,
hintText: '어떤 주제로 대화하고 싶으신가요?',
onChanged: (value) {
if (value.length > 1) {
var lastChar = value.substring(
value.length - 1);
if (!hashRegex.hasMatch(lastChar)) {
var newHash = value.substring(
0, value.length - 1);
if (!_hashList.contains(newHash)) {
setState(() {
_hashList.add(newHash);
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
}
_controller.clear();
}
} // else if (_expHash.length == 3 && value.isNotEmpty) {
_formKey.currentState!.validate();
},
validator: (value) {
if (!hashRegex.hasMatch(value!)) {
return '\u26a0 한글, 영문만 가능해요';
}
return null;
}
),
DefaultSpacing(),
Row(
children: [
Text('추천 : ',
style: TextStyle(
fontSize: 10.sp,
color: Colors.grey[800],
),
),
Wrap(
spacing: 6.0,
runSpacing: 6.0,
children: _suggestionList.map((suggestion) =>
HashtagSuggestionChip(
suggestion: suggestion,
type: 'topic',
onPressed: () {
if (!_hashList.contains(suggestion)) {
setState(() {
_hashList.add(suggestion);
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
}
},
)).toList(),
),
],
),
DefaultSpacing(),
Wrap(
spacing: 6.0,
runSpacing: 6.0,
children: _hashList.map((hashtag) =>
HashtagChip(
hashtag: hashtag,
type: 'topic',
onDelete: () {
setState(() {
_hashList.remove(hashtag);
_formKey.currentState!.validate();
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
},
)).toList()
),
]),
);
}
return CircularProgressIndicator();
}
),
],
),
floatingActionButton: FloatingSaveButton(context: context,
text: '저장',
width: size.width * 0.9,
disabled: _disabled,
onPressed: () async {
_userProfileService.updateTopicHashtag(hashList: _hashList);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(SaveSnackBar());
}),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
Result: when the page is first opened
Neither hashtag nor progress indicator is shown.
After text form field is selected
I searched similar questions in stackoverflow, but none of the answers solved my problem.
Try like this
#override
void initState() {
setState(() {
_future = HashtagService().getSuggestion('topic');
_hashList = widget._userProfile.topic.cast<String>();
});
super.initState();
}
so your _future is async so the widget will be loaded before loading the service
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();
}
}
I have a ListView.builder that builds a certain amount of widgets depending on user input. Each widget has their own specific name and has a DropDownMenu. I save this value with the corresponding name of the widget. It saves it correctly. However, when I try and read the data and create a new list from it, this error appears: [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: NoSuchMethodError: The method '[]' was called on null.
'course' is a list. I am using the shared preferences import. When you tap the flat button, it should build the new list, but it is not. Could you explain to me why this is not working please?
This is code in the main app.
void main() {
runApp(Hemis());
}
class Hemis extends StatefulWidget {
#override
_HemisState createState() => _HemisState();
}
class _HemisState extends State<Hemis> {
_read() async {
final prefs = await SharedPreferences.getInstance();
for(int i = 0; i < course.length; i++) {
listMarks[i].name = course[i].name;
listMarks[i].mark = prefs.getInt(course[i].name) ?? 0;
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
ListView.builder(
itemCount: course.length,
itemBuilder: (context, index) {
return ModuleListItem(
name: '${course[index].name}',
credits: course[index].credits,
);
},
),
FlatButton(
onPressed: () {
_read();
for(int i = 0; i < course.length; i++) {
print('${listMarks[i].name}: ${listMarks[i].mark}');
}
},
),
],
),
)
)
);
}
}
The widget that is being built.
final percentage = List<String>.generate(100, (i) => "$i");
class ModuleListItem extends StatefulWidget {
const ModuleListItem ({ Key key, this.name, this.credits }): super(key: key);
final String name;
final int credits;
#override
_ModuleListItemState createState() => _ModuleListItemState();
}
class _ModuleListItemState extends State<ModuleListItem> {
String dropdownValue;
bool isSwitched = false;
_save() async {
final prefs = await SharedPreferences.getInstance();
final key = '${widget.name}';
final value = int.parse(dropdownValue);
prefs.setInt(key, value);
print('saved $value');
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
DropdownButton<String>(
value: dropdownValue,
icon: Icon(Icons.keyboard_arrow_down),
onChanged: (String newValue) {
setState(() {
dropdownValue = newValue;
});
},
items: percentage.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
Switch(
value: isSwitched,
onChanged: (value) {
setState(() {
isSwitched = value;
if(isSwitched == true) {
_save();
}
print(isSwitched);
});
},
),
],
),
);
}
}
One of the requirements of my current project is a multi-page sign-up form, with each page performing its own independent validation. The way I ended up deciding to implement this is by splitting it up into several smaller forms, which you can pass some functions in as parameters to be executed when the form validation and/or value change runs. Below is an example of one of the "mini-forms" I created to implement this. This one collects information about the user's name.
import "package:flutter/material.dart";
class SignUpNameForm extends StatefulWidget {
final Function(String) onFirstNameChange;
final Function(String) onLastNameChange;
final Function(bool) onFirstNameValidationStatusUpdate;
final Function(bool) onLastNameValidationStatusUpdate;
final String firstNameInitialValue;
final String lastNameInitialValue;
SignUpNameForm(
{this.onFirstNameChange,
this.onLastNameChange,
this.onFirstNameValidationStatusUpdate,
this.onLastNameValidationStatusUpdate,
this.firstNameInitialValue,
this.lastNameInitialValue});
#override
_SignUpNameFormState createState() => _SignUpNameFormState();
}
class _SignUpNameFormState extends State<SignUpNameForm> {
final _formKey = GlobalKey<FormState>();
TextEditingController _firstName;
TextEditingController _lastName;
bool _editedFirstNameField;
bool _editedLastNameField;
#override
Widget build(BuildContext context) {
_firstName.text = widget.firstNameInitialValue;
_lastName.text = widget.lastNameInitialValue;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: _firstName,
autovalidate: true,
decoration: InputDecoration(
hintText: "First Name",
),
keyboardType: TextInputType.text,
onChanged: (String value) {
_editedFirstNameField = true;
widget.onFirstNameChange(value);
},
validator: (String value) {
String error;
if (_editedFirstNameField) {
error = value.isEmpty ? "This field is required" : null;
bool isValid = error == null;
widget.onFirstNameValidationStatusUpdate(isValid);
}
return error;
},
),
TextFormField(
controller: _lastName,
autovalidate: true,
decoration: InputDecoration(
hintText: "Last Name",
),
keyboardType: TextInputType.text,
onChanged: (String value) {
_editedLastNameField = true;
widget.onLastNameChange(value);
},
validator: (String value) {
String error;
if (_editedLastNameField) {
error = value.isEmpty ? "This field is required" : null;
bool isValid = error == null;
widget.onLastNameValidationStatusUpdate(isValid);
}
return error;
},
),
],
),
);
}
#override
void initState() {
super.initState();
_firstName = new TextEditingController();
_lastName = new TextEditingController();
_editedFirstNameField = false;
_editedLastNameField = false;
}
#override
void dispose() {
super.dispose();
_firstName.dispose();
_lastName.dispose();
}
}
Then, in my widget that displays the form components, I do something like this.
class _SignUpFormState extends State<SignUpForm> {
String _firstName;
bool _firstNameIsValid;
String _lastName;
bool _lastNameIsValid;
int current;
final maxLength = 4;
final minLength = 0;
#override
Widget build(BuildContext context) {
// create the widget
return Container(
child: _showFormSection(),
);
}
/// Show the appropriate form section
Widget _showFormSection() {
Widget form;
switch (current) {
case 0:
form = _showNameFormSection();
break;
case 1:
form = _showEmailForm();
break;
case 2:
form = _showPasswordForm();
break;
case 3:
form = _showDobForm();
break;
}
return form;
}
// shows the name section of the form.
Widget _showNameFormSection() {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
child: SignUpNameForm(
firstNameInitialValue: _firstName,
lastNameInitialValue: _lastName,
onFirstNameChange: (String value) {
setState(() {
_firstName = value;
});
},
onFirstNameValidationStatusUpdate: (bool value) {
setState(() {
_firstNameIsValid = value;
});
},
onLastNameChange: (String value) {
setState(() {
_lastName = value;
});
},
onLastNameValidationStatusUpdate: (bool value) {
setState(() {
_lastNameIsValid = value;
});
},
),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FlatButton(
child: Text("Next"),
onPressed: (_firstNameIsValid && _lastNameIsValid)
? _showNextSection
: null,
),
],
),
)
],
),
);
}
/// Shows the next section in the form
void _showNextSection() {
print("Current Section: " + current.toString());
setState(() {
if (current >= maxLength) {
current = maxLength;
} else {
current++;
}
});
print("Next Section: " + current.toString());
}
/// Show the previous section of the form
void _showPreviousSection() {
print("Current Section:" + current.toString());
setState(() {
if (current <= minLength) {
current = minLength;
} else {
current--;
}
print("Previous Section: " + current.toString());
});
}
#override
void initState() {
super.initState();
current = 0;
_firstName = "";
_firstNameIsValid = false;
_lastName = "";
_lastNameIsValid = false;
// other initializations
}
}
As you can see here, I pass in functions to extract the values of the user's name, as well as the status of the validation, and use that to determine whether or not I should enable the "next" button in the form handler widget. This is now causing a problem, specifically because the functions I pass into the "mini-form" invokes initState() while the widget is being rebuilt.
How might I go about handling this? Or, is there a better way I can go about implementing this multi-Page form that is cleaner?
Thanks.
build() should be fast and idempotent. You should not be calling setState() inside a build. Imagine build() is being called 60 times a second (although it won't be thanks to optimizations) and you'll have the proper mindset.
I'm new on Flutter.
I'm trying to disabled button while I compile textFormField and it is invalid.
My problem is that it works only if I click on "confirm" on keyboard and not while I compile my input.
So would like disable button while I write the input.
I've done a pastebin for example:
https://pastebin.com/q5WuwrCm
class AddCartButton extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return AddCartButtonState();
}
}
class AddCartButtonState extends State<AddCartButton>{
TextEditingController myController = TextEditingController();
bool isValid = false;
#override
Widget build(BuildContext context) {
void _addToCart(){
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("QUANTITY"),
content: Column(
children: <Widget>[
TextFormField(
controller: myController,
decoration: new InputDecoration(labelText: "quantity"),
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: <TextInputFormatter>[],
autovalidate: true,
validator: (value) {
if (value.isEmpty) {
isValid = false;
return "the quantity cannot be empty";
} else if (double.tryParse(value) == null) {
isValid = false;
return "the quantity must be valid number";
} else {
isValid = true;
return null;
}
}
)
],
),
actions: <Widget>[
FlatButton(
disabledTextColor: Colors.grey,
child: Text("add"),
onPressed: isValid ? () { print("is valid"); }: null
)
],
);
},
);
}
}
}
You can use addListener() function on your TextEditingController myController.
myController.addListener((){
//With this, you can "listen" all the changes on your text while
//you are typing on input
print("value: ${myController.text}");
//use setState to rebuild the widget
setState(() {
//you can check here if your text is valid or no
//_isValidText() is just an invented function that returns
//a boolean representing if the text is valid or not
if(_isValidText(myController.text)) isValid = true;
else isValid = false;
});
});
If I understood what you want to achieve the following code works for you. The conditions I set are the same as your validation rules:
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
TextEditingController _textEditingController = TextEditingController();
Function _submitFunction;
// We need a reusable timer and it needs to not be null so that we can cancel it from th start.
Timer _timer = Timer(Duration(milliseconds: 1), () => print('initialTimer'));
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Auto disable button'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
controller: _textEditingController,
onChanged: (text) {
setState(() {
_submitFunction = null;
});
restartTimer();
},
),
RaisedButton(
child: Text('Submit'),
onPressed: _submitFunction,
),
],
),
),
)
);
}
void submitForm(){
print(_textEditingController.text);
}
void enableButton(){
if(_textEditingController.text != ''){
setState(() {
_submitFunction = submitForm;
});
}
}
void restartTimer(){
_timer.cancel();
_timer = Timer(Duration(milliseconds: 1500), enableButton);
}
}
You can use this method .
bool isEnable = false;
void validateButton() {
bool isValid = true;
isValid = userEmail.isNotEmpty &&
userPassword.isNotEmpty &&
validateEmail(userEmail) &&
userPassword.length >= 8;
setState(() {
isEnable = isValid;
});
}
now in your Textfield onChanged method you have to call this function
like This
onChanged: (email) {
userEmail = email;
setState(() {});
validateButton();
},
and in your Login Button
isEnable?ActiveButton():DisableButton()