I have a TextFormField that, if the user leaves it, should fill with specific Text.
So for example, if the user edits the field and enters "Test", and I don't want to allow the String "Test", then as soon as the field looses focus it should replace the Text with "Sike!". So something like onChanged, but the event being the loss of Focus, not a change of value, which is what I had so far:
TextFormField(
controller: myController,
onChanged: (value) {
if (value == "Test"){
myController.text = "Sike!";
}
},
.............
.............
One way you can do this is like so.
class _DemoState extends State<Demo> {
final node = FocusNode();
final tc = TextEditingController();
#override
void initState() {
node.addListener(() {
if (!node.hasFocus && tc.text == 'Test') {
tc.text = "Sike!";
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: tc,
focusNode: node,
);
}
}
Related
I am trying to make essentially a Flutter multi-select input with an auto-complete search box, and I'm attempting to use the Material Autocomplete class with a custom User type to do so. The behavior I am after but having trouble with is for the text input to remain unchanged, even as the user makes selections.
What's making this a bit difficult is the displayStringForOption property. This property only takes the instance of the custom type corresponding to the user's selection, and as far as I can tell, nothing that indicates what the current text input is. This causes the input to be overwritten when the user makes a selection, which I would like to avoid. I don't believe the TextEditingController is available either.
Here's an example of what I have at the moment:
#override
Widget build(BuildContext context) =>
Autocomplete<User>(
fieldViewBuilder: _inviteeSearchInput,
displayStringForOption: (u) => '${u.name} - ${u.email}', // <== actually want this to just remain as the user's input
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return widget.availableUsers.where((user) => !_selectedUsers.contains(user));
}
return widget.availableUsers.where((User u) =>
!_selectedUsers.contains(u) && (u.name.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
u.email.toLowerCase().contains(textEditingValue.text.toLowerCase())));
},
optionsViewBuilder: ...,
onSelected: (u) {
setState(() {
_selectedUsers = { ..._selectedUsers, u };
});
},
);
Widget _inviteeSearchInput(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) => TextFormField(
controller: textEditingController, // <== the TextEditingController is here, but I don't think that helps?
focusNode: focusNode,
decoration: ...
);
One thought I've tried that seems to work, but for some reason doesn't feel right is to keep track of the user input in a variable and update it with the onChanged property of the fieldViewBuilder:
class MultiSelectAutoCompleteState extends State<MultiSelectAutoComplete> {
Set<User> _selectedUsers = {};
String inputVal = ''; // <== var to keep track of user's input
// ...
#override
Widget build(BuildContext context) =>
Autocomplete<User>(
fieldViewBuilder: _inviteeSearchInput,
displayStringForOption: (u) => inputVal, // <== using that as the display string
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return widget.availableUsers.where((user) => !_selectedUsers.contains(user));
}
return widget.availableUsers.where((User u) =>
!_selectedUsers.contains(u) && (u.name.toLowerCase().contains(textEditingValue.text.toLowerCase()) ||
u.email.toLowerCase().contains(textEditingValue.text.toLowerCase())));
},
optionsViewBuilder: ...,
onSelected: (u) {
setState(() {
_selectedUsers = { ..._selectedUsers, u };
});
},
);
Widget _inviteeSearchInput(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) => TextFormField(
controller: textEditingController,
onChanged: (s) => inputVal = s, // <== setting the var here
focusNode: focusNode,
decoration: ...
);
I'm not calling setState in the onChanged callback because I'm not sure I want to trigger a rebuild every time the input changes, but maybe I do?
I'm curious if there's a better way to do this, for some reason, this feels icky to me, and I'm also having a hard time reasoning about whether or not I want to call setState in the onChanged callback if I do go with the second option.
I want to update a list of type SongModel when I enter a value in the TextField. However, the list is not updating when onChanged is called.
List<SongModel> songs = item.data!;
List<SongModel> filterSongs = [];
//showing the songs
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
keyboardType: TextInputType.text,
controller: searchController,
onChanged: (value) {
//pass value for the search
getSearch(filterSongs,songs);
},
decoration: InputDecoration(............
getSearch() :
getSearch(List<SongModel> filterSongs,List<SongModel> songs)
{
var text = searchController.text;
if (text.isEmpty) {
setState(() {
filterSongs = songs;
});
}
print(songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList());
print(text);
setState(() {
// search = text;
filterSongs = songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList();
});
print(filterSongs.length);
}
Here the list is not updating with set state method.
In your getSearch method, you are setting the value of the parameter passed to getSearch, not the value of the list outside of that method. You should move the filterSongs list declaration outside of your build method anyways so that it isn't redeclared every time the screen is rebuilt.
class MyScreenClassState extends State<MyScreenClass>{
//Create State method here
List<SongModel> filterSongs = [];
//Build method here
}
getSearch(List<SongModel> songs)
{
var text = searchController.text;
if (text.isEmpty) {
setState(() {
filterSongs = songs;
});
}
print(songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList());
print(text);
setState(() {
// search = text;
filterSongs = songs.where((SongModel item) => item.title.toLowerCase().contains(text.toLowerCase())).toList();
});
print(filterSongs.length);
}
If onChanged method doesn't work for your implementation, you can try this structure I think I will work for you.
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.addListener(_onControllerChange);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
void _onControllerChange() async {
.....
}
If I update a variable using class object, the build method should get called, but I am unable to call setState from the StatefulWidget class.
class CustomErrorFormField extends StatefulWidget {
#override
_CustomErrorFormFieldState createState() {
return _CustomErrorFormFieldState();
}
List<String> errorList = []; //this variable will get updated using below function
void setErrorList(List<String> listOfError) {
errorList = listOfError;
}
}
class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
print(widget.errorList); //this is not printing updated value
return .....
}
}
Now in some other class i will update errorList Variable
nameTextFild = CustomErrorFormField(
key: ValueKey(count),
labelName: "Name",
iContext: context,
onChanged: (String value) {
setState(() {
count++;
if (!value.contains(RegExp(r'[0-9]'))) {
nameTextFild!.setErrorList([]); //updating but changes not appearing (setState of this widget is not getting called)
} else {
nameTextFild!.setErrorList(["Invalid characters, use letters only."]);
}
});
},
);
It's not recommended that you change the state of a widget from outside the widget.
What you should do instead is pass the validation logic as a function and let the widget handle the state change.
CustomFormField:
import 'package:flutter/material.dart';
class CustomErrorFormField extends StatefulWidget {
//Take the validation logic as a parameter.
final List<String> Function(String value) validator;
const CustomErrorFormField({required this.validator});
#override
_CustomErrorFormFieldState createState() {
return _CustomErrorFormFieldState();
}
}
class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
//Keep the state inside the widget itself
List<String> errorList = [];
//Update the state from inside the widget
void setErrorList(List<String> listOfError) {
setState(() {
errorList = listOfError;
});
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Form(
child: TextFormField(
validator: (String value){
//Use the validation logic to decide the error.
setErrorList(widget.validator(value))
}
}
),
);
}
}
I have used TextFormField as an example, you can use any widget that accepts a callback upon change.
If you're making everything from scratch you can attach the validator function to a callback that fires when the text is changed. Usually this is done with the help of a controller.
usage:
final nameTextFild = CustomErrorFormField(
key: ValueKey(count),
labelName: "Name",
iContext: context,
validator: (String value) {
if (!value.contains(RegExp(r'[0-9]'))) {
return [];
} else {
return ["Invalid characters, use letters only."];
}
},
);
I am trying to create a TextFormField with increment and decrement buttons and TextFormField is editable "by hand" as well. But there is a small problem if I use BLoC with this - state "falls" one behind, meaning that when I tap "+" first time nothing changes, but when I tap it the second time it changes its value to 21 (and so on..).
I tried the same implementation with just a regular Text and it works as expected and updating properly.
I'm just wondering if my logic of how I am setting TextFormField is flawed:
Instantiating TextEditingController with default value amount (20);
On "+" tap:
Adding PlusEvent to increment current value
Getting amount value from state
Widget class:
class MyCalculation extends StatefulWidget {
const MyCalculation({Key? key}) : super(key: key);
#override
State<MyCalculation> createState() => _MyCalculationState();
}
class _MyCalculationState extends State<MyCalculation> {
late TextEditingController _controller;
late MyCalcBloc _bloc;
#override
void initState() {
super.initState();
_bloc = context.read();
_controller.text = _bloc.state.amount.toString();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<MyCalcBloc, MyCalcState>(builder: (context, state) {
return MyCustomTextFormField(
controller: _controller,
onChanged: (value) {},
onPlusTap: () {
_bloc.add(PlusEvent());
_bloc.text = '${state.amount}';
},
onMinusTap: () {});
});
}
}
BLoC class:
class MyCalcBloc extends Bloc<MyCalcEvent, MyCalcState> {
MyCalcBloc() : super(const MyCalcState(amount: 20)) {
on<IncrementFromEvent>(_onPlusEvent);
}
void _onPlusEvent(PlusEvent event, Emitter<MyCalcState> emit) {
final newValue = state.amount + 1;
emit(MyCalcState(amount: newValue));
}
}
You should instantiate TextEditingController within BlocProvider, that way you'll get "current" state value displayed in TextFormField.
#override
Widget build(BuildContext context) {
return BlocBuilder<MyCalcBloc, MyCalcState>(builder: (context, state) {
_controller = TextEditingController(text: state.amount.toString());
return MyCustomTextFormField(
controller: _controller,
onChanged: (value) {},
onPlusTap: () {
_bloc.add(PlusEvent());
_bloc.text = '${state.amount}';
},
onMinusTap: () {});
});
}
Currently working on a way to translate romaji to hiragana directly in my TextField, I'v managed to get something working:
class RomajiTextInput extends StatefulWidget{
final bool mustConvertToKana;
const RomajiTextInput({Key key, this.mustConvertToKana}) : super(key: key);
#override
State<StatefulWidget> createState() => _RomajiTextInputState();
}
class _RomajiTextInputState extends State<RomajiTextInput> {
TextEditingController _titleEditingController;
TextEditingController _hiddenTitleEditingController;
String previousValue = "";
#override
void initState() {
super.initState();
_titleEditingController = new TextEditingController();
_hiddenTitleEditingController = new TextEditingController();
}
#override
void dispose() {
super.dispose();
_titleEditingController.clear();
_hiddenTitleEditingController.clear();
}
void onConversionChanged(String text){
if (widget.mustConvertToKana){
_hiddenTitleEditingController.text = getRomConversion(text, onlyRomaji: false);
String japanese = getJapaneseTranslation(_hiddenTitleEditingController.text, hasSpace: true);
int cursor = getCursorPosition(previousValue, japanese);
setState(() {
_titleEditingController.text = japanese;
_titleEditingController.selection = TextSelection.fromPosition(TextPosition(offset: japanese.length));
previousValue = japanese;
});
}
}
#override
Widget build(BuildContext context) {
return TextField(
controller: _titleEditingController,
onChanged: (text){
onConversionChanged(text);
},
decoration: InputDecoration(
labelText: 'Question',
labelStyle: TextStyle(fontSize: 20),
hintText:
'Enter a question / a word to remember'),
);
}
}
However, I would like to create a widget using these properties but also which can override all the different fields of a TextField such as "decoration", add some logic to the "onChanged" method or even add other functions from the TextField Widget such as "onEditingComplete".
In fact, I would like to be able to do something like this:
RomajiTextInput(
controller: myChildController,
onChanged: (text){print('text');} //And still convert to hiragana thanks to "onConversionChanged"
decoration: InputDecoration(...) //All options that have not been mentioned are kept otherwise just added
)
I bet there is a way to do so but extending the TextField Widget didn't get me anywhere...
Thank in advance :)