I'm working on a CustomFormField class to customize the default behaviour of the TextFormField. One of the things I wanted to do was move the position of the errorText to prevent it from adjusting the position of other fields on the form in the event of an error. I have achieved half of my goal by using a stack and repositioning the errorText to appear on the lower border of the field instead of a several pixels below. Unfortunately the default behaviour still reserves the space and the other fields move anyway.
Text Fields without error condition
Text Fields with error condition
As you can see the errorText is no longer taking up space but the default behaviour of the TextFormField takes the space anyway.
Is there a way to prevent this?
It's a little long and this is still a work in progress but here is the code I'm using.
class CustomFormField extends StatefulWidget {
final GlobalKey formKey;
final TextEditingController controller;
final FocusNode currentFocusNode;
final FocusNode futureFocusNode;
final TextInputType inputType;
final TextInputAction inputAction;
final bool allowShowPassword;
final bool enabled;
final bool autoFocus;
final String labelText;
final String hintText;
final FormFieldValidator<String>? validator;
final Function? onChanged;
final FormFieldSetter<String>? onSaved;
const CustomFormField(
{super.key,
required this.formKey,
required this.controller,
required this.currentFocusNode,
required this.futureFocusNode,
this.inputType = TextInputType.text,
this.inputAction = TextInputAction.next,
this.allowShowPassword = true,
this.enabled = true,
this.autoFocus = false,
this.labelText = '',
this.hintText = '',
this.validator,
this.onChanged,
this.onSaved});
#override
State<CustomFormField> createState() => _CustomFormFieldState();
}
class _CustomFormFieldState extends State<CustomFormField> {
bool _passwordHidden = true;
bool onError = false;
String _errorText = '';
#override
Widget build(BuildContext context) {
// grab the application theme to use when colouring text fields
ThemeData theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Stack(
children: [
TextFormField(
enabled: widget.enabled,
autofocus: widget.autoFocus,
textInputAction: widget.inputAction,
onEditingComplete: () =>
FocusScope.of(context).requestFocus(widget.futureFocusNode),
focusNode: widget.currentFocusNode,
keyboardType: widget.inputType,
obscureText: (widget.inputType == TextInputType.visiblePassword &&
_passwordHidden)
? _passwordHidden
: false,
controller: widget.controller,
validator: (value) {
String? errorText = widget.validator!.call(value);
if (errorText != null && Static.formIsValid) {
Static.formIsValid = false;
widget.currentFocusNode.requestFocus();
setState(() {
onError = true;
_errorText = errorText;
});
} else {
setState(() {
onError = false;
_errorText = '';
});
}
// the validator still needs us to return null if there was no error
// but if there is an error return empty string
return errorText == null ? null : '';
},
onSaved: widget.onSaved,
onChanged: (value) {
if (widget.onChanged != null) {
widget.onChanged!.call(value);
}
},
style: TextStyle(
color: widget.enabled
? Constants.formFieldColor
: theme.disabledColor,
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Ariel'),
decoration: InputDecoration(
labelText: widget.labelText,
hintText: widget.hintText,
hintStyle: const TextStyle(color: Colors.white38),
contentPadding: const EdgeInsets.symmetric(horizontal: 25),
// constraints: const BoxConstraints(maxHeight: 40, minHeight: 40),
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: Colors.black)),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: Colors.black)),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: Colors.black)),
errorBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: Colors.red)),
focusedErrorBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: Colors.black)),
disabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide(color: Colors.black)),
alignLabelWithHint: true,
focusColor: Colors.black,
suffixIcon: widget.inputType == TextInputType.visiblePassword &&
widget.allowShowPassword
? InkWell(
onTap: () {
setState(() {
_passwordHidden = !_passwordHidden;
});
},
child: Icon(
_passwordHidden
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Constants.passwordShowHideIconColor,
size: 18,
),
)
: null,
),
),
// this repositions the errorText to appear just on top of the lower border
onError
? Positioned(
bottom: 17,
left: 25,
child: Container(
height: 15,
decoration: const BoxDecoration(color: Colors.white),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text(_errorText,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
fontStyle: FontStyle.normal)),
),
),
)
: Container(),
],
),
);
}
}
One thing I did try was wrapping it all in a SizedBox. This did stop other fields from changing their positions but it had the side affect of shrinking the size of the field with the error because it always wants that space for the errorText.
Any help would be appreciated.
Solved:
I was able to solve this by adding:
errorStyle: const TextStyle(height: 0),
This stopped the field from taking additional space for the error text.
Related
I have mu custom DropDown Widget.
class DropDownMenuField extends StatefulWidget {
String title;
final InputDecoration decoration;
final String Function(String) validator;
final void Function(dynamic value) onSelectionDone;
final String initialValue;
final Color selectedItemColor;
final DropMenuMode menuMode;
int selectvalueindex;
final Function(String) onChanged;
var model;
DropDownMenuField(
{Key key,
this.title,
this.decoration,
this.validator,
this.onSelectionDone,
this.initialValue,
this.selectedItemColor,
this.model,
this.menuMode,
this.selectvalueindex,
this.onChanged})
: super(key: key);
#override
State<DropDownMenuField> createState() => _DropDownMenuFieldState();
}
class _DropDownMenuFieldState extends State<DropDownMenuField> {
final TextEditingController _controller = TextEditingController();
void _popUpModalBottomSheet(BuildContext context) async {
List<String> finalDataList = [];
List<dynamic> dataMap = await apiRegistrationDataList(widget.menuMode);
finalDataList = parseMap(dataMap);
final result = await showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9.0),
),
isScrollControlled: true,
context: context,
builder: (context) => DialogMenu(
title: widget.title,
data: finalDataList,
));
if (result == null) {
widget.title = widget.title;
_controller.text = widget.title;
} else {
// widget.title = result ?? widget.title;
// lab
labStyle = checkedLableStyle;
lable = result;
_controller.text = result;
widget.onSelectionDone(result);
widget.validator(result);
setState(() {});
}
SetGlobalRequaerData(result, dataMap);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _popUpModalBottomSheet(context),
child: TextFormField(
controller: _controller,
readOnly: true,
enabled: false,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: widget.validator,
onChanged: widget.onChanged,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF3C3C43),
),
decoration: InputDecoration(
labelStyle: labStyle,
hintStyle: TextStyle(
fontFamily: 'FiraSans',
color: GeneralStyles.greyColor,
),
fillColor: Colors.white,
filled: true,
contentPadding: const EdgeInsets.fromLTRB(16, 0, 0, 0),
errorStyle: const TextStyle(
color: Colors.red,
fontFamily: 'FiraSans',
fontWeight: FontWeight.w400,
fontSize: 13),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
color: GeneralStyles.greyColor.withOpacity(0.5),
width: .5)),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(color: Color(0xFF707070), width: .5)),
disabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(4)),
borderSide:
BorderSide(color: GeneralStyles.greyColor, width: .5)),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(4)),
borderSide:
BorderSide(color: GeneralStyles.blackColor, width: 1)),
errorBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(color: Colors.red, width: .5),
),
labelText: lable,
hintText: widget.title,
suffixIcon: Icon(
Icons.keyboard_arrow_down,
color: GeneralStyles.greyColor,
),
),
));
}
}
And i call it on my registration page.
DropDownMenuField(
title: "Anrede",
model: widget.model,
menuMode: DropMenuMode.genders,
selectvalueindex: 0,
onSelectionDone: (value){
print(value);
},
validator: (value){
if(widget.model.selectedDropMenu[0].isEmpty){
return 'Bitte fülle das Feld aus';
}
return null;
},
),
When i click button when DropDownMenuField selectedValue is empty validator is work but when i select value widget don't call validator and don't change color from red. How call validator after change value. I read that when texteditingcontroller is changed, calling validator after him. i add it but not work.
I have created profile setup page where I have name, email and phone text field. I have created single textfield class where I take controller, title as parameter. How can I create a validator by checking the email or a phone in a single textfield class.
This is how I created in view class
ProfileTextFormField(
controller: _mobileNumberController,
title: "Mobile No",
hintText: "",
),
ProfileTextFormField(
controller: _emailIdController,
title: "Email Id",
hintText: "",
),
Below is the class for textfield where I intake input. I need to validate here for email, or phone.
class ProfileTextFormField extends StatelessWidget {
final title;
final hintText;
final TextEditingController controller;
const ProfileTextFormField(
{Key? key,
required this.title,
required this.hintText,
required this.controller})
: super(key: key);
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 3.0, left: 24),
child: Text(
title,
style: AppTextStyle.latoMedium(
fontWeight: FontWeight.w700, color: AppColorPallet.profileText),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Padding(
padding: EdgeInsets.only(bottom: 5.0, right: 24, left: 24),
child: TextField(
controller: controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColorPallet.app_green_natural,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColorPallet.app_Red,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColorPallet.app_green_natural,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColorPallet.app_green_natural,
),
),
hintText: hintText,
),
),
),
),
],
);
}
}
You can pass validator as parameter too.Try this:
class TextFormFieldWidget extends StatelessWidget {
final TextEditingController controller;
final String? title;
final String? Function(String?)? validator;
TextFormFieldWidget({
required this.controller,
this.validator,
this.title,
});
#override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: title,
),
validator: validator,
);
}
}
and use it like this:
TextFormFieldWidget(
controller: TextEditingController(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'empty value';
}
return null;
})
I am creating a form which contain more than 10 textboxes, so i created a widget "textformfield" and call it, that's called reusable widget. I want a icon on every textbox too, that will be vary according to textbox content.
here is what i done
import 'package:flutter/material.dart';
//import 'package:attendance_system_app/new.dart';
class Textformfield extends StatelessWidget {
final String hinttext;
final double font;
final Color fontcolor;
final Icon icon;
final FontWeight fontweight;
final TextEditingController controller;
const Textformfield({
Key key,
this.hinttext,
this.font,
this.icon,
this.fontcolor,
this.fontweight,
this.controller
}) : super(key: key);
#override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText:hinttext,
isDense: true,
contentPadding: EdgeInsets.fromLTRB(10, 10, 10, 0),
prefixIcon: Icon(
icon == null ? Container() :
icon,
//color: Colors.blue,
),
hintStyle: TextStyle(
fontStyle: FontStyle.normal,
fontSize: 16,
color: Colors.grey,
),
enabledBorder: new OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: new BorderSide(
color: Colors.black, width: 2,),
),
focusedBorder: new OutlineInputBorder(
borderSide: new BorderSide(
color: Colors.black, width: 2),
),
));
}
}
but it gives me error on icon , here is the snap of error
how to create a textfield with centered hint text and suffix icon?
i make centered hint with TextAlign.center
the problem is that when i add an suffix icon hint does not stay in center and moves to left
Well, this is working fine for me.
If it doesn't work, maybe is something else affecting your UI.
Please try this snippet.
TextField(
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: 'Center the text',
suffixIcon: IconButton(
icon: Icon(Icons.add),
onPressed: () {
debugPrint('click bait');
}
),
),
),
Try with a below code snippet that has prefix and sufixicon in a textformField
Create a Textfield widget like a below:
import 'package:flutter/material.dart';
import 'package:row_nation/Utils/app_colors.dart';
import 'package:row_nation/Utils/app_font_size.dart';
import 'package:row_nation/Utils/app_font_weight.dart';
class PassWordTextFormFieldWidget extends StatelessWidget {
final TextEditingController controllerName;
final String hintTxt;
final TextInputType keyboardType;
final Color cursorColor;
final Function(String) onChange;
final Function(String) onSaved;
final String? Function(String?)? validatorData;
final IconData prefixIcon;
final IconData suffixIcon;
final Function() sufficIconTap;
PassWordTextFormFieldWidget({
super.key,
required this.controllerName,
required this.hintTxt,
required this.prefixIcon,
required this.keyboardType,
required this.cursorColor,
required this.onChange,
required this.onSaved,
required this.validatorData,
required this.suffixIcon,
required this.sufficIconTap,
});
#override
Widget build(BuildContext context) {
double? height, width;
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Container(
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.kEmptyDotColor.withOpacity(0.4),
),
child: TextFormField(
controller: controllerName,
cursorColor: cursorColor,
obscureText: true,
textAlign: TextAlign.left,
keyboardType: keyboardType,
style: Theme.of(context).textTheme.caption?.copyWith(
color: AppColors.kWhiteColor,
letterSpacing: 0.2,
fontSize: AppFontSize.fourteenFontSize,
fontWeight: AppFontWeight.sixHundredFont,
),
validator: (value) {
// widget.validatorData!(value);
return validatorData!(value);
},
onChanged: (va) {
onChange(va);
},
onSaved: (val) {
print(val);
},
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
isDense: true,
hintText: hintTxt,
hintStyle: Theme.of(context).textTheme.caption?.copyWith(
color: AppColors.kIconColor,
fontSize: AppFontSize.twelveFontSize,
fontWeight: AppFontWeight.fourHundredFont,
),
// When user gets Error
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.kRedColor,
),
),
// When user getting error and focuses on a textformfield
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.kRedColor,
),
),
// When user Focuses on textformField widget
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.kSplashBackColor,
),
),
// Default TextformField Color
enabledBorder: InputBorder.none,
suffixIcon: GestureDetector(
onTap: () {
sufficIconTap();
},
child: Icon(
suffixIcon,
size: 15,
color: AppColors.kIconColor,
),
),
prefixIcon: Icon(
prefixIcon,
size: 15,
color: AppColors.kIconColor,
),
// border: InputBorder.none,
),
),
);
}
}
and use it wherever like a below :
PassWordTextFormFieldWidget(
controllerName: passwordController,
prefixIcon: Icons.lock,
suffixIcon: Icons.visibility_off,
sufficIconTap: () {
print("Visibility Icon Tapped");
},
hintTxt: AppStrings.passwordTxt,
keyboardType: TextInputType.text,
cursorColor: AppColors.kSplashBackColor,
onChange: (p0) {},
onSaved: (p0) {},
validatorData: (p0) {},
),
Don't forget to upvote if found useful.
i have created multiple text field using a single method in different file how i retrieve the value from them. I want to retrieve the value in different variables.
//class method
class CustomTextField {
static TextField display(BuildContext context, [String name, double size=16,IconData icon]) {
return new TextField(
textAlign: TextAlign.center,
style: CustomTextStyle.display(context, Colors.black38, size),
decoration: new InputDecoration(
alignLabelWithHint: true,
icon: new Icon(icon),
contentPadding:EdgeInsets.only(top: 30,right: 30,),
border: InputBorder.none,
hintText:"$name",
focusedBorder: InputBorder.none,
hintStyle: CustomTextStyle.display(context, Colors.grey, size),
),
);
}
}
//call method
new Container(
width: width,
height: height,
decoration: new BoxDecoration(
borderRadius: new BorderRadius.all(Radius.circular(20)),
gradient: new LinearGradient(
colors: [
Color.fromRGBO(252, 191, 93, 1),
Color.fromRGBO(255, 210, 119, 1),
Color.fromRGBO(252, 215, 85, 1),
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
child: CustomTextField.display(context, name,16,icon),
),
First of all, I recommend that you use Stateless Widget to create your TextField instead of using a static function like this code below:
class CustomTextField extends StatelessWidget {
final Function(String) onChanged;
final String name;
final double size;
final IconData icon;
CustomTextField({
this.onChanged,
this.name,
this.size: 16,
this.icon,
});
Widget build(BuildContext context) {
return new TextField(
textAlign: TextAlign.center,
onChanged: onChanged,
style: CustomTextStyle.display(context, Colors.black38, size),
decoration: new InputDecoration(
alignLabelWithHint: true,
icon: new Icon(icon),
contentPadding: EdgeInsets.only(
top: 30,
right: 30,
),
border: InputBorder.none,
hintText: "$name",
focusedBorder: InputBorder.none,
hintStyle: CustomTextStyle.display(context, Colors.grey, size),
),
);
}
}
Then, create a function for the onChange field to receive the new value:
class App extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: width,
height: height,
decoration: new BoxDecoration(
borderRadius: new BorderRadius.all(Radius.circular(20)),
gradient: new LinearGradient(
colors: [
Color.fromRGBO(252, 191, 93, 1),
Color.fromRGBO(255, 210, 119, 1),
Color.fromRGBO(252, 215, 85, 1),
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
child: CustomTextField(
// Here you get the value change
onChanged: (value) {
print('Text value changed');
print('New value: $value');
},
name: name,
size: 16,
icon: icon,
),
),
);
}
}
Use Function as a parameter for your CustomTextField and add it to onChanged parameter of TextField;
class CustomTextField {
static TextField display(BuildContext context, Function onChanged,
[String name, double size = 16, IconData icon]) {
return new TextField(
textAlign: TextAlign.center,
onChanged: onChanged,
style: CustomTextStyle.display(context, Colors.black38, size),
decoration: new InputDecoration(
alignLabelWithHint: true,
icon: new Icon(icon),
contentPadding: EdgeInsets.only(
top: 30,
right: 30,
),
border: InputBorder.none,
hintText: "$name",
focusedBorder: InputBorder.none,
hintStyle: CustomTextStyle.display(context, Colors.grey, size),
),
);
}
}
then get your value;
CustomTextField.display(
context,
(value) {
print(value);
},
name,
16,
icon,
),
you should provide a function to handle the onChange property of your custom text field.
here is how i used one in a project of mine:
the custom class widget
class MyTextField extends StatefulWidget {
final String title;
final Function onChange; // you can get the value from this function
final bool isPassword;
MyTextField(this.title, this.onChange, {this.isPassword = false});
#override
_MyTextFieldState createState() => _MyTextFieldState();
}
class _MyTextFieldState extends State<MyTextField> {
bool showPassword = false;
final _controller = TextEditingController();
#override
Widget build(BuildContext context) {
_controller.addListener(() {
setState(() {});
});
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
SizedBox(height: 2),
TextField(
controller: _controller,
onChanged: widget.onChange,
obscureText: !showPassword && widget.isPassword,
decoration: InputDecoration(
suffixIcon: widget.isPassword
? IconButton(
icon: Icon(
Icons.remove_red_eye,
color: showPassword ? Colors.blue : Colors.grey,
),
onPressed: () {
setState(() => showPassword = !showPassword);
},
)
: IconButton(
icon: Icon(
Icons.clear,
color: _controller.text.isEmpty
? Colors.grey
: Colors.blue,
),
onPressed: () => _controller.clear()),
border: InputBorder.none,
fillColor: Color(0xfff3f3f4),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 5.0),
),
filled: true),
)
],
),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
the use case:
MyTextField('Email', (value) => email = value.trim()), // body of onChange
MyTextField(
'Password',
(value) => password = value.trim(), // body of onChange
isPassword: true,
),
// value is what you get from the text fields.
Provide a TextEditingController as a parameter to construct your CustomTextField.