I am trying to create an overlay widget for TextFormField suffix Icon. Normally we would be using ToolTip, but just trying something new because the overlay widget can be customized. I want to change the suffix Icon Color of TextFormField if it is not validated from Grey to Red. So when the Icon becomes red it alerts the user that something is wrong, when the user clicks on it overlay widget will be shown.
My Code for OverLay widget.
void _showOverlay(BuildContext context) async {
OverlayState? overlayState = Overlay.of(context);
OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(builder: (context) {
return Positioned(
left: MediaQuery.of(context).size.width * 0.1,
top: MediaQuery.of(context).size.height * 0.23,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Material(
child: Container(
alignment: Alignment.center,
color: Colors.grey.shade200,
padding:
EdgeInsets.all(MediaQuery.of(context).size.height * 0.02),
width: MediaQuery.of(context).size.width * 0.8,
height: MediaQuery.of(context).size.height * 0.06,
child: const Text(
'Name should be more than 2 characters',
style: TextStyle(color: Colors.black),
),
),
),
),
);
});
overlayState!.insert(overlayEntry);
await Future.delayed(const Duration(seconds: 3));
overlayEntry.remove();
}
My Submit Button method:
void _submitForm() {
setState(() {
_autoValidateMode = AutovalidateMode.always;
});
final form = _formKey.currentState;
if (form == null || !form.validate()) return;
form.save();
print(_name);
}
My TextFormField widget:
TextFormField(
controller: nameController,
keyboardType: TextInputType.name,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.words,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return;
}
return null;
},
onSaved: (String? value) {
_name = value;
},
decoration: kTextInputDecoration.copyWith(
labelText: 'Full Name',
prefixIcon: const Icon(Icons.person),
suffixIcon: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
_showOverlay(context);
},
icon: const Icon(
Icons.info,
color: Colors.grey //change icon color according to form validation
))),
My submit button.
ElevatedButton(
onPressed: () {
_submitForm();
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(10)),
child: const Text(
'Submit',
style: TextStyle(fontSize: 20),
)),
I want to change the color of the suffix icon color when the submit button is pressed. If the form is not validated the color should change to red or the default is grey. Thank you very much in advance for your help.
Warning: the below solution is currently broken on the stable channel 3.3.9 but it works fine in beta.
The issue was marked as fixed on August 23, 2022 so I have good hope that we will get it in stable soon.
You can:
create a class which extends MaterialStateColor
create an instance in your build method based on your theme
pass the instance to suffixIconColor
class InfoIconColor extends MaterialStateColor {
const InfoIconColor(
super.defaultColor, {
required this.disabledColor,
required this.errorColor,
});
final Color disabledColor;
final Color errorColor;
#override
Color resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.error)) return errorColor;
if (states.contains(MaterialState.disabled)) return disabledColor;
return Color(super.value);
}
}
TextFormField(
// ...
decoration: kTextInputDecoration.copyWith(
labelText: 'Full Name',
suffixIcon: // ...
suffixIconColor: // ...
),
)
In the meantime, you can can use the same InfoIconColor in the theme of your app (or wrap your widget with a theme widget) like so:
final theme = Theme.of(context);
Theme(
data: theme.copyWith(
inputDecorationTheme: theme.inputDecorationTheme.copyWith(
suffixIconColor: //...
),
),
child: //...
)
You can create bool to store the validation or directly use the validate method.
///state level
bool _isValidate = true;
......
/// inside TextFormField
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
setState(() {
_isValidate = false;
});
return "Error message";
}
return null;
},
onTap: () {
setState(() {
_isValidate = true;
});
},
/// and Icon color
icon: Icon(
Icons.info,
color: _isValidate ? Colors.grey : Colors.red,
),
I'm trying to create a dynamic form so I used the idea of using a listview builder to create it. I was able to successfully create it but I faced that I cannot discard changes made to the form by popping it off after editing it. The two textFormField Job name and rate per hour were able to discard changes as they were using onsaved but on the checkbox I can't do that as it has onChanged which wraps setstate to change its state.
You can take a look at the video at this link to see how it functions as of now - https://vimeo.com/523847256
As you can see that it is retaining the data even after popping the page and coming back which I don't want it to. I'm looking for a way to prevent that and make the form the same as before if the user didn't press save.
I have tried to reassign the variables() in onpressed of back button but that didn't work. I also tried push replacement to the same page to reset it but that also didn't work. I think the cuprit here is the sublist and the initialValueTextFormField and initialValueCheckbox which are used declared under ListView.builder but I don't know how to fix that without affecting the dynamic list functionality.
class EditJobPage extends StatefulWidget {
const EditJobPage({Key key, this.job}) : super(key: key);
final Job job;
static Future<void> show(BuildContext context, {Job job}) async {
await Navigator.of(context, rootNavigator: true).pushNamed(
AppRoutes.editJobPage,
arguments: job,
);
}
#override
_EditJobPageState createState() => _EditJobPageState();
}
class _EditJobPageState extends State<EditJobPage> {
final _formKey = GlobalKey<FormState>();
String _name;
int _ratePerHour;
List<dynamic> _subList = [];
Set newSet = Set('', false);
#override
void initState() {
super.initState();
if (widget.job != null) {
_name = widget.job?.name;
_ratePerHour = widget.job?.ratePerHour;
_subList = widget.job?.subList;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 2.0,
title: Text(widget.job == null ? 'New Job' : 'Edit Job'),
leading: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: <Widget>[
FlatButton(
child: const Text(
'Save',
style: TextStyle(fontSize: 18, color: Colors.white),
),
onPressed: () => _submit(),
),
],
),
body: _buildContents(),
backgroundColor: Colors.grey[200],
);
}
Widget _buildContents() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _buildForm(),
),
),
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildFormChildren(),
),
);
}
List<Widget> _buildFormChildren() {
print(_subList);
return [
TextFormField(
decoration: const InputDecoration(labelText: 'Job name'),
keyboardAppearance: Brightness.light,
initialValue: _name,
validator: (value) =>
(value ?? '').isNotEmpty ? null : 'Name can\'t be empty',
onChanged: (value) {
setState(() {
_name = value;
});
},
),
TextFormField(
decoration: const InputDecoration(labelText: 'Rate per hour'),
keyboardAppearance: Brightness.light,
initialValue: _ratePerHour != null ? '$_ratePerHour' : null,
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
onChanged: (value) {
setState(() {
_ratePerHour = int.tryParse(value ?? '') ?? 0;
});
},
),
Column(
children: <Widget>[
ListView.builder(
shrinkWrap: true,
itemCount: _subList?.length ?? 0,
itemBuilder: (context, index) {
String initialValueTextFormField =
_subList[index].subListTitle.toString();
bool initialValueCheckbox = _subList[index].subListStatus;
return Row(
children: [
Checkbox(
value: initialValueCheckbox,
onChanged: (bool newValue) {
setState(
() {
initialValueCheckbox = newValue;
_subList.removeAt(index);
_subList.insert(
index,
Set(initialValueTextFormField,
initialValueCheckbox));
},
);
},
),
Expanded(
child: TextFormField(
minLines: 1,
maxLines: 1,
initialValue: initialValueTextFormField,
autofocus: false,
textAlign: TextAlign.left,
onChanged: (title) {
setState(() {
initialValueTextFormField = title;
_subList.removeAt(index);
_subList.insert(
index,
Set(initialValueTextFormField,
initialValueCheckbox));
});
},
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelStyle: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
),
filled: true,
hintText: 'Write sub List here',
),
),
),
],
);
},
),
TextButton(
onPressed: () {
setState(() {
_subList.add(newSet);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.add),
Text('Add Sub Lists'),
],
),
),
],
),
];
}
void _submit() {
final isValid = _formKey.currentState.validate();
if (!isValid) {
return;
} else {
final database = context.read<FirestoreDatabase>(databaseProvider);
final id = widget.job?.id ?? documentIdFromCurrentDate();
final job = Job(
id: id,
name: _name ?? '',
ratePerHour: _ratePerHour ?? 0,
subList: _subList);
database.setJob(job);
Navigator.of(context).pop();
}
}
}
And this is the link to the full repository of the whole flutter app in case you want to look at any other part:- https://github.com/brightseagit/dynamic_forms . Thank you.
Note - This is the edited code of this repo - https://github.com/bizz84/starter_architecture_flutter_firebase.
When assigning the list we need to use _subList = List.from(widget.job.subList) instead of _subList = widget.job.subList.
Otherwise, the changes made in _subList will also be made in job.subList .
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: () {
},
)