Am working on an app that has a form that uses ListViewBuilder for the list of the items and returns a widget that shows a TextField for each item in the list. Is there a way to access the TextField Controller of any item from the form widget itself to get the new value?
Widget used in the ListView builder
class BrandNamesWidget extends StatelessWidget {
BrandNamesWidget({Key? key, required this.brandName, required this.index})
: super(key: key);
final String brandName;
final int index;
final TextEditingController _brandNameController =
new TextEditingController();
#override
Widget build(BuildContext context) {
_brandNameController.text = brandName;
final deviceDimensions = Provider.of<Dimension>(context);
double height = deviceDimensions.getDeviceHeight();
double width = deviceDimensions.getDeviceWidth();
return TextField(
controller: _brandNameController,
cursorColor: Colors.black,
style: TextStyle(
color: AppInfo.MAIN_COLOR,
letterSpacing: 1.5,
),
keyboardType: TextInputType.text,
decoration: InputDecoration(
constraints: BoxConstraints(maxWidth: width * 0.45),
labelText: 'Brand name: ' + index.toString(),
prefixIcon: Icon(Icons.medication),
labelStyle: TextStyle(
fontSize: height * 0.03,
color: AppInfo.MAIN_COLOR,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(50),
),
),
);
}
}
Form Widget
.
.
Container(
child: ListView.builder(
itemCount: _brandnames.length,
itemBuilder: (BuildContext ctx, int index) {
return BrandNamesWidget(
brandName: _brandnames[index].toString(),
index: index);
}),
),
// Want to access the _brandNameController.text here in a function to update firestore
.
.
I have checked online for solutions and the only similar question I found was from this stackoverflow question (Is there a way to access variables in a state in Flutter/Dart?)
One way to do it is instantiating a TextEditingController from the outside in the itemBuilder and inject it inside the BrandNamesWidget, hold their references in a collection, that way you can grab their content from the outside like this Gist I created for you. This is what your new class would look like:
class BrandNamesWidget extends StatelessWidget {
final TextEditingController? controller;
final String? brandName;
final int? index;
BrandNamesWidget({ this.brandName, required this.index, this.controller });
#override
Widget build(BuildContext context) {
double height = 100;
double width = MediaQuery.of(context).size.width;
return TextField(
controller: controller!,
keyboardType: TextInputType.text,
decoration: InputDecoration(
constraints: BoxConstraints(maxWidth: width * 0.45),
labelText: 'Brand name: ' + index.toString(),
prefixIcon: const Icon(Icons.medication),
labelStyle: TextStyle(
fontSize: height * 0.03,
color: Colors.grey,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(50),
),
),
);
}
}
And this is the parent widget holding on to the TextEditingController references, housing your ListView.builder:
class MyWidget extends StatelessWidget {
List<String> _brandnames = ['Toyota', 'Honda', 'BMW', 'Mercedes'];
List<TextEditingController> controllers = [];
#override
Widget build(BuildContext context) {
controllers = [];
return Column(
children: [
Expanded(
child: Container(
child: ListView.builder(
itemCount: _brandnames.length,
itemBuilder: (BuildContext ctx, int index) {
TextEditingController ctrl = TextEditingController();
controllers.add(ctrl);
return BrandNamesWidget(
brandName: _brandnames[index].toString(),
controller: ctrl,
index: index);
}),
)
),
TextButton(
onPressed: () {
for(var ctrl in controllers) {
print(ctrl.text);
}
},
child: Text('Click me to get their values')
)
]
);
}
}
If you input stuff on the text fields shown, and you click on the button I provided, you'll get their content from a function, thus being able to update Firestore or anything you want. Check it out and let me know.
I am creating a login form which has username and password field, i want to add validation when user skip any field. I have created this reusable textfield.
class RoundedInputField extends StatelessWidget {
final String hintText;
final ValueChanged<String> onChanged;
final TextEditingController controller;
final FormFieldValidator validate;
const RoundedInputField({Key key, this.hintText,
this.onChanged,
this.controller,this.validate,
})
: super(key: key);
#override
Widget build(BuildContext context) {
return TextFieldContainer(
child: TextFormField(
onChanged: onChanged,
controller: TextEditingController(),
validator: validate,
decoration: InputDecoration(
hintText: hintText,
border: InputBorder.none,
),
),
);
}
}
and calling it like this
RoundedInputField(hintText: "Username",icon: Icons.email,fontsize: 20,
controller: TextEditingController(text: user.username),
onChanged: (value){
user.username=value;
},
validate: (value){
if(value.isEmpty){
return "This field is required";
}
},
),
but validator property is not working properly, here it is.
please help if anyone know how to fix it!
I have been using TextFormField in a reusable way like this , it has served me for all the purposes i needed , i think it works for your case too
class BoxTextField extends StatelessWidget {
final TextEditingController controller;
final FormFieldValidator<String> validator;
final bool obsecure;
final bool readOnly;
final VoidCallback onTap;
final VoidCallback onEditingCompleted;
final TextInputType keyboardType;
final ValueChanged<String> onChanged;
final bool isMulti;
final bool autofocus;
final bool enabled;
final String errorText;
final String label;
final Widget suffix;
final Widget prefix;
BoxTextField(
{Key key,
this.controller,
this.validator,
this.keyboardType = TextInputType.text,
this.obsecure = false,
this.onTap,
this.isMulti = false,
this.readOnly = false,
this.autofocus = false,
this.errorText,
#required this.label,
this.suffix,
this.prefix,
this.enabled = true,
this.onEditingCompleted,
this.onChanged})
: super(key: key);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 4),
child: TextFormField(
onChanged: onChanged,
onEditingComplete: onEditingCompleted,
autofocus: autofocus,
minLines: isMulti ? 4 : 1,
maxLines: isMulti ? null : 1,
onTap: onTap,
enabled: enabled,
readOnly: readOnly,
obscureText: obsecure,
keyboardType: keyboardType,
controller: controller,
decoration: InputDecoration(
errorText: errorText,
prefixIcon: prefix,
suffixIcon: suffix,
labelStyle: TextStyle(fontSize: lableFontSize()),
labelText: label,
hintStyle: TextStyle(color: Colors.blueGrey, fontSize: 15),
contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 20),
enabledBorder: textFieldfocused(),
border: textFieldfocused(),
focusedBorder: textFieldfocused(),
errorBorder: errorrTextFieldBorder(),
focusedErrorBorder: errorrTextFieldBorder(),
),
validator: validator),
);
}
}
This is the Usage
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
TextEditingController _emailPhone = new TextEditingController();
TextEditingController _password = new TextEditingController();
GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
#override
void initState() {
super.initState();
}
String loginError;
bool loggingIn = false;
#override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 50,
),
BoxTextField(
validator: (str) {
if (str.isEmpty) {
return tr('common.required');
}
return null;
},
controller: _emailPhone,
label: tr('login.username'),
),
SizedBox(
height: 10,
),
BoxTextField(
label: tr('login.password'),
controller: _password,
obsecure: true,
validator: (str) {
if (str.isEmpty) {
return tr('common.required');
}
return null;
},
),
Center(
child: BoxButton(
loading: loggingIn,
lable: tr('login.btn'),
onPressed: () {
},
)),
],
)),
)
]))
],
);
}
}
Replace your code and try this:
RoundedInputField(
hintText: "Username",
icon: Icons.email,
fontsize: 20,
controller: TextEditingController(text: user.username),
onChanged: (value) {
user.username = value;
},
validate: (value) {
if (value.isEmpty) {
return "This field is required";
}
return null;
},
),
I have created a custom controller for TextFormField, but I use it in my widgets it clears the input upon releasing focus from the input. the same setup works fine with the local widget set up with the same but I want to have a common widget to use throughout the application for better code maintainability. How can have the input intact entered into the input field??
Custom Input Widget-
import 'package:HSSE/screens/constant.dart';
import 'package:flutter/material.dart';
class AppInput extends StatelessWidget {
const AppInput(
{Key key,
this.correct = false,
this.hintText,
this.inputLabel,
this.labelFont = 18.0,
this.labelFontWeight,
this.validators,
this.keyboard,
this.length,
this.icon,
this.trailingIcon,
this.readOnly = false,
this.inputController,
this.onTap = null,
this.onInputValueChange,
this.labelText})
: super(key: key);
final bool correct;
final String hintText;
final String labelText;
final String inputLabel;
final double labelFont;
final FontWeight labelFontWeight;
final Function validators;
final Function onInputValueChange;
final TextInputType keyboard;
final int length;
final Icon icon;
final Icon trailingIcon;
final bool readOnly;
final Function onTap;
final TextEditingController inputController;
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Color(0Xffcacaca), width: 0.75),
borderRadius: BorderRadius.circular(10),
),
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
labelText,
style: TextStyle(fontSize: labelFont),
),
TextFormField(
readOnly: readOnly,
keyboardType: keyboard,
maxLength: length,
controller: inputController,
cursorColor: apmt_color,
decoration: InputDecoration(
prefixIcon: icon,
suffixIcon: trailingIcon,
errorStyle: TextStyle(
fontSize: 16,
),
border: InputBorder.none,
hintText: hintText,
labelStyle: TextStyle(
color: Colors.black,
),
fillColor: Colors.red,
),
style: TextStyle(color: Colors.black),
onChanged: (value) {
onInputValueChange(value);
},
validator: (value) {
return validators(value);
},
)
],
),
);
}
}
When I use it into my other widget like below -
final TextEditingController emailCtrl = TextEditingController();
AppInput(
inputController: emailCtrl,
keyboard: TextInputType.emailAddress,
hintText: Localizer.of(context).translate('inspectorNameHint'),
labelText: Localizer.of(context).translate('inspectorName'),
validators: (String name) {
if (name.isEmpty)
return Localizer.of(context)
.translate('inspectorNameError');
},
)
the input gets clears when the focus gets removed from the input field it works fine with the same setup on the local widget but I want to make a common widget to optimize the code.
Is there a proper way to add a clear button to the TextField?
Just like this picture from Material design guidelines:
What I found is to set a clear IconButton in the InputDecoration's suffixIcon. Is this the right way?
Output:
Create a variable
var _controller = TextEditingController();
And your TextField:
TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Enter a message',
suffixIcon: IconButton(
onPressed: _controller.clear,
icon: Icon(Icons.clear),
),
),
)
Container(
margin: EdgeInsets.only(left: 16.0),
child: TextFormField(
controller: _username,
decoration: InputDecoration(
hintText: '请输入工号',
filled: true,
prefixIcon: Icon(
Icons.account_box,
size: 28.0,
),
suffixIcon: IconButton(
icon: Icon(Icons.remove),
onPressed: () {
debugPrint('222');
})),
),
),
use iconButton
Try this -
final TextEditingController _controller = new TextEditingController();
new Stack(
alignment: const Alignment(1.0, 1.0),
children: <Widget>[
new TextField(controller: _controller,),
new FlatButton(
onPressed: () {
_controller.clear();
},
child: new Icon(Icons.clear))
]
)
Here’s another answer expanding a bit on #Vilokan Lab’s answer, which wasn’t really doing it for me since FlatButton has a minimum width of 88.0, and thus the clear button was not appearing right-aligned with the TextField at all.
So I went ahead and made my own button class, and applied that using a Stack, here is my process:
Button class:
class CircleIconButton extends StatelessWidget {
final double size;
final Function onPressed;
final IconData icon;
CircleIconButton({this.size = 30.0, this.icon = Icons.clear, this.onPressed});
#override
Widget build(BuildContext context) {
return InkWell(
onTap: this.onPressed,
child: SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment(0.0, 0.0), // all centered
children: <Widget>[
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.grey[300]),
),
Icon(
icon,
size: size * 0.6, // 60% width for icon
)
],
)));
}
}
Then apply like so as InputDecoration to your TextField:
var myTextField = TextField(
controller: _textController,
decoration: InputDecoration(
hintText: "Caption",
suffixIcon: CircleIconButton(
onPressed: () {
this.setState(() {
_textController.clear();
});
},
)),
},
);
To get this:
Unhighlighted state
Highlighted / selected state.
Note this colouring comes free when you use suffixIcon.
Note you can also Stack it in your TextField like this, but you won't get the auto-colouring you get when you use suffixIcon:
var myTextFieldView = Stack(
alignment: Alignment(1.0,0.0), // right & center
children: <Widget>[
TextField(
controller: _textController,
decoration: InputDecoration(hintText: "Caption"),
),
Positioned(
child: CircleIconButton(
onPressed: () {
this.setState(() {
_textController.clear();
});
},
),
),
],
);
Search TextField with icon and clear button
import 'package:flutter/material.dart';
class SearchTextField extends StatefulWidget{
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return new SearchTextFieldState();
}
}
class SearchTextFieldState extends State<SearchTextField>{
final TextEditingController _textController = new TextEditingController();
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Row(children: <Widget>[
new Icon(Icons.search, color: _textController.text.length>0?Colors.lightBlueAccent:Colors.grey,),
new SizedBox(width: 10.0,),
new Expanded(child: new Stack(
alignment: const Alignment(1.0, 1.0),
children: <Widget>[
new TextField(
decoration: InputDecoration(hintText: 'Search'),
onChanged: (text){
setState(() {
print(text);
});
},
controller: _textController,),
_textController.text.length>0?new IconButton(icon: new Icon(Icons.clear), onPressed: () {
setState(() {
_textController.clear();
});
}):new Container(height: 0.0,)
]
),),
],);
}
}
TextFormField(
controller:_controller
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: (){
_controller.clear();
},
icon: Icon(
Icons.keyboard,
color: Colors.blue,
),
),
),
)
TextEditingController is used to check the current state of Text, where we can decide whether we can show the cancel icon or not, depend on the availability of Text.
var _usernameController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: TextField(
controller: _usernameController,
onChanged: (text) {
setState(() {});
},
decoration: InputDecoration(
labelText: 'Username',
suffixIcon: _usernameController.text.length > 0
? IconButton(
onPressed: () {
_usernameController.clear();
setState(() {});
},
icon: Icon(Icons.cancel, color: Colors.grey))
: null),
),
),
),
);
}
Output:
Here's a snippet of my code that works.
What it does: only show clear button if text field value is not empty
class _MyTextFieldState extends State<MyTextField> {
TextEditingController _textController;
bool _wasEmpty;
#override
void initState() {
super.initState();
_textController = TextEditingController(text: widget.initialValue);
_wasEmpty = _textController.text.isEmpty;
_textController.addListener(() {
if (_wasEmpty != _textController.text.isEmpty) {
setState(() => {_wasEmpty = _textController.text.isEmpty});
}
});
}
#override
void dispose() {
_textController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: _textController,
decoration: InputDecoration(
labelText: widget.label,
suffixIcon: _textController.text.isNotEmpty
? Padding(
padding: const EdgeInsetsDirectional.only(start: 12.0),
child: IconButton(
iconSize: 16.0,
icon: Icon(Icons.cancel, color: Colors.grey,),
onPressed: () {
setState(() {
_textController.clear();
});
},
),
)
: null,
),);
...
TextField(
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
Icons.cancel,
),
onPressed: () {
_controllerx.text = '';
}
),
)
)
To add icon inside the textfield. You must have to use suffixIcon or prefixIcon inside the Input decoration.
TextFormField(
autofocus: false,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: Icon(
Icons.clear,
size: 20.0,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(0.0)),
),
hintText: 'Enter Password',
contentPadding: EdgeInsets.all(10.0),
),
);
Didn't want to go the StatefulWidget route. Here's an example using TextEditingController and StatelessWidget (with Providers pushing updates).
I keep the controller in the static field.
class _SearchBar extends StatelessWidget {
static var _controller = TextEditingController();
#override
Widget build(BuildContext context) {
var dictionary = Provider.of<Dictionary>(context);
return TextField(
controller: _controller,
autofocus: true,
onChanged: (text) {
dictionary.lookupWord = text;
},
style: TextStyle(fontSize: 20.0),
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Search',
suffix: GestureDetector(
onTap: () {
dictionary.lookupWord = '';
_controller.clear();
},
child: Text('x'),
)));
}
}
If you want a ready-to-use Widget, which you can just put in a file and then have a reusable element you can use everywhere by inserting ClearableTextField(), use this piece of code:
import 'package:flutter/material.dart';
class ClearableTexfield extends StatefulWidget {
ClearableTexfield({
Key key,
this.controller,
this.hintText = 'Enter text'
}) : super(key: key);
final TextEditingController controller;
final String hintText;
#override
State<StatefulWidget> createState() {
return _ClearableTextfieldState();
}
}
class _ClearableTextfieldState extends State<ClearableTexfield> {
bool _showClearButton = false;
#override
void initState() {
super.initState();
widget.controller.addListener(() {
setState(() {
_showClearButton = widget.controller.text.length > 0;
});
});
}
#override
Widget build(BuildContext context) {
return TextField(
controller: widget.controller,
decoration: InputDecoration(
hintText: widget.hintText,
suffixIcon: _getClearButton(),
),
);
}
Widget _getClearButton() {
if (!_showClearButton) {
return null;
}
return IconButton(
onPressed: () => widget.controller.clear(),
icon: Icon(Icons.clear),
);
}
}
Further explanations can be found on this page:
https://www.flutterclutter.dev/flutter/tutorials/text-field-with-clear-button/2020/104/
It also builds upon IconButton, but has the advantage of only displaying the clear button when there is text inside the textfield.
Looks like this:
You can also use TextFormField.
First create Form Key. final _formKeylogin = GlobalKey<FormState>();
Form(key: _formKeylogin,
child: Column(
children: [
Container(
margin: EdgeInsets.all(20.0),
child: TextFormField(
style: TextStyle(color: WHITE),
textInputAction: TextInputAction.next,
onChanged: (companyName) {
},
validator: (companyName) {
if (companyName.isEmpty) {
return 'Please enter company Name';
} else {
return null;
}
},
),
And in OnTap or onPress Method.
_formKeylogin.currentState.reset();
To clear a button when the input it is not empty and with focus.
Create a controller variable:
//Clear inputs
final _nameInputcontroller = TextEditingController();
Create a FocusNode:
late FocusNode focusNodeNameInput;
#override
void initState() {
super.initState();
//FocusNode for all inputs
focusNodeNameInput = FocusNode();
focusNodeNameInput.addListener(() {
setState(() {});
});
}
#override
void dispose() {
// Clean up the focus node when the Form is disposed.
focusNodeNameInput.dispose();
super.dispose();
}
And the TextFormField:
TextFormField(
controller: _nameInputcontroller,
focusNode: focusNodeNameInput,
decoration: InputDecoration(
labelText: LocaleKeys.name.tr(),
labelStyle: TextStyle(
color: focusNodeNameInput.hasFocus ? AppColors.MAIN_COLOR : null,
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.MAIN_COLOR,
),
),
suffixIcon: _nameInputcontroller.text.isNotEmpty || focusNodeNameInput.hasFocus
? IconButton(
onPressed: _nameInputcontroller.clear,
icon: const Icon(
Icons.clear,
color: AppColors.MAIN_COLOR,
),
) : null,
),
),
Robust solution based on the code written by the Flutter team
Here is a fully reusuable ClearableTextFormField with maximum configuration, most of the code for this clearable text field here is from the commits on Apr 1, 2021 of the Flutter team for the built-in TextFormField. The ClearableTextFormField accepts the same params as and works similarly to the built-in TextFormField.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// A [TextFormField] with a clear button
class ClearableTextFormField extends FormField<String> {
/// Creates a [FormField] that contains a [TextField].
///
/// When a [controller] is specified, [initialValue] must be null (the
/// default). If [controller] is null, then a [TextEditingController]
/// will be constructed automatically and its `text` will be initialized
/// to [initialValue] or the empty string.
///
/// For documentation about the various parameters, see the [TextField] class
/// and [new TextField], the constructor.
ClearableTextFormField({
Key? key,
this.controller,
String? initialValue,
FocusNode? focusNode,
InputDecoration decoration = const InputDecoration(),
TextInputType? keyboardType,
TextCapitalization textCapitalization = TextCapitalization.none,
TextInputAction? textInputAction,
TextStyle? style,
StrutStyle? strutStyle,
TextDirection? textDirection,
TextAlign textAlign = TextAlign.start,
TextAlignVertical? textAlignVertical,
bool autofocus = false,
bool readOnly = false,
ToolbarOptions? toolbarOptions,
bool? showCursor,
String obscuringCharacter = '•',
bool obscureText = false,
bool autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
bool enableSuggestions = true,
MaxLengthEnforcement? maxLengthEnforcement,
int? maxLines = 1,
int? minLines,
bool expands = false,
int? maxLength,
ValueChanged<String>? onChanged,
GestureTapCallback? onTap,
VoidCallback? onEditingComplete,
ValueChanged<String>? onFieldSubmitted,
FormFieldSetter<String>? onSaved,
FormFieldValidator<String>? validator,
List<TextInputFormatter>? inputFormatters,
bool? enabled,
double cursorWidth = 2.0,
double? cursorHeight,
Radius? cursorRadius,
Color? cursorColor,
Brightness? keyboardAppearance,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
bool enableInteractiveSelection = true,
TextSelectionControls? selectionControls,
InputCounterWidgetBuilder? buildCounter,
ScrollPhysics? scrollPhysics,
Iterable<String>? autofillHints,
AutovalidateMode? autovalidateMode,
ScrollController? scrollController,
// Features
this.resetIcon = const Icon(Icons.close),
}) : assert(initialValue == null || controller == null),
assert(obscuringCharacter.length == 1),
assert(
maxLengthEnforcement == null,
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1,
'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength > 0),
super(
key: key,
initialValue:
controller != null ? controller.text : (initialValue ?? ''),
onSaved: onSaved,
validator: validator,
enabled: enabled ?? true,
autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled,
builder: (FormFieldState<String> field) {
final _ClearableTextFormFieldState state =
field as _ClearableTextFormFieldState;
final InputDecoration effectiveDecoration = decoration
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
void onChangedHandler(String value) {
field.didChange(value);
if (onChanged != null) onChanged(value);
}
return Focus(
onFocusChange: (hasFocus) => state.setHasFocus(hasFocus),
child: TextField(
controller: state._effectiveController,
focusNode: focusNode,
decoration: effectiveDecoration.copyWith(
errorText: field.errorText,
suffixIcon:
((field.value?.length ?? -1) > 0 && state.hasFocus)
? IconButton(
icon: resetIcon,
onPressed: () => state.clear(),
color: Theme.of(state.context).hintColor,
)
: null,
),
keyboardType: keyboardType,
textInputAction: textInputAction,
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textAlignVertical: textAlignVertical,
textDirection: textDirection,
textCapitalization: textCapitalization,
autofocus: autofocus,
toolbarOptions: toolbarOptions,
readOnly: readOnly,
showCursor: showCursor,
obscuringCharacter: obscuringCharacter,
obscureText: obscureText,
autocorrect: autocorrect,
smartDashesType: smartDashesType ??
(obscureText
? SmartDashesType.disabled
: SmartDashesType.enabled),
smartQuotesType: smartQuotesType ??
(obscureText
? SmartQuotesType.disabled
: SmartQuotesType.enabled),
enableSuggestions: enableSuggestions,
maxLengthEnforcement: maxLengthEnforcement,
maxLines: maxLines,
minLines: minLines,
expands: expands,
maxLength: maxLength,
onChanged: onChangedHandler,
onTap: onTap,
onEditingComplete: onEditingComplete,
onSubmitted: onFieldSubmitted,
inputFormatters: inputFormatters,
enabled: enabled ?? true,
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
scrollPadding: scrollPadding,
scrollPhysics: scrollPhysics,
keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: enableInteractiveSelection,
selectionControls: selectionControls,
buildCounter: buildCounter,
autofillHints: autofillHints,
scrollController: scrollController,
),
);
},
);
/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController] and
/// initialize its [TextEditingController.text] with [initialValue].
final TextEditingController? controller;
final Icon resetIcon;
#override
_ClearableTextFormFieldState createState() => _ClearableTextFormFieldState();
}
class _ClearableTextFormFieldState extends FormFieldState<String> {
TextEditingController? _controller;
bool hasFocus = false;
TextEditingController get _effectiveController =>
widget.controller ?? _controller!;
#override
ClearableTextFormField get widget => super.widget as ClearableTextFormField;
#override
void initState() {
super.initState();
if (widget.controller == null)
_controller = TextEditingController(text: widget.initialValue);
else
widget.controller!.addListener(_handleControllerChanged);
}
#override
void didUpdateWidget(ClearableTextFormField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null)
_controller =
TextEditingController.fromValue(oldWidget.controller!.value);
if (widget.controller != null) {
setValue(widget.controller!.text);
if (oldWidget.controller == null) _controller = null;
}
}
}
#override
void dispose() {
widget.controller?.removeListener(_handleControllerChanged);
super.dispose();
}
#override
void didChange(String? value) {
super.didChange(value);
if (_effectiveController.text != value)
_effectiveController.text = value ?? '';
}
#override
void reset() {
// setState will be called in the superclass, so even though state is being
// manipulated, no setState call is needed here.
_effectiveController.text = widget.initialValue ?? '';
super.reset();
}
void setHasFocus(bool b) => setState(() => hasFocus = b);
void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
if (_effectiveController.text != value)
didChange(_effectiveController.text);
}
/// Invoked by the clear suffix icon to clear everything in the [FormField]
void clear() {
WidgetsBinding.instance!.addPostFrameCallback((_) {
_effectiveController.clear();
didChange(null);
});
}
}
Read more about the assert statement in the Dart language doc.
Below are the explanations for the additional code added to the built-in TextFormField
Optional resetIcon param for the ClearableTextFormField
🚀 Additional code inside _ClearableTextFormFieldState:
/// Invoked by the clear suffix icon to clear everything in the [FormField]
void clear() {
WidgetsBinding.instance!.addPostFrameCallback((_) {
_effectiveController.clear();
didChange(null);
});
}
addPostFrameCallback() ensures the passed-in function is only called on Widget build complete (when the TextFormField has been fully built and rendered on the screen). U can read more about it in this thread.
The following state and setState allows u to keep track of the focus change in TextField (when the field is focused or unfocused):
bool hasFocus = false;
void setHasFocus(bool b) => setState(() => hasFocus = b);
🚀 Additional code inside the builder method of the extended FormField<String> (the super() parts):
Hide the clear button when the field's value is empty or null:
suffixIcon:
((field.value?.length ?? -1) > 0 && state.hasFocus)
? IconButton(
icon: resetIcon,
onPressed: () => state.clear(),
color: Theme.of(state.context).hintColor,
)
: null,
Rebuild the TextField on focus and unfocus to show and hide the clear button:
Focus(
onFocusChange: (hasFocus) => state.setHasFocus(hasFocus),
child: TextField(// more code here...),
)
IconButton(
icon: Icon(Icons.clear_all),
tooltip: 'Close',
onPressed: () {
},
)