Flutter - How to implement inline editing? - flutter

I found a similar behaviour on Safari:

The problem statement is that on tap of normal text, the text field should be editable and we can able to edit the text.
Have an example
In this scenario, we can see how on tap of initial text the editable text field is getting displayed and we can able to edit the text.
So let’s start with the process:
First, we have to initialize the variables.
bool _isEditingText = false;
TextEditingController _editingController;
String initialText = "Initial Text";
_isEditingText is the boolean variable and we have to set it false because we have to make it true when the user is tap on text.
TextEditingController -whenever a user modifies a text field with an associated TextEditingController, the text field edits and the controller notifies the listener. Listeners can then read the text and selection properties that the user has typed and updated. Basically the text editing controller is used to get the updated value from the text field.
initialText -Initial value, set to the text.
When we use any type of controller then we have to initialize and dispose of the controller.
So first initialize the controller in init state.
#override
void initState() {
super.initState();
_editingController = TextEditingController(text: initialText);
}
#override
void dispose() {
_editingController.dispose();
super.dispose();
}
‘dispose()’ is called when the State object is removed, which is permanent.
This method is used to unsubscribe and cancel all animations, streams, etc.
The framework calls this method when this state object will never build again.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("'Editable Text"),
),
body: Center(
child: _editTitleTextField(),
),
);
}
In build widget, I am simply displaying a widget _editTitleTextField().
Widget _editTitleTextField() {
if (_isEditingText)
return Center(
child: TextField(
onSubmitted: (newValue){
setState(() {
initialText = newValue;
_isEditingText =false;
});
},
autofocus: true,
controller: _editingController,
),
);
return InkWell(
onTap: () {
setState(() {
_isEditingText = true;
});
},
child: Text(
initialText,
style: TextStyle(
color: Colors.black,
fontSize: 18.0,
),
);
}
so what exactly the _editTitleTextField() widget does is, if the value of _isEditingText is false then simply show the text and on tap of text set the value of _isEditingText to true.
When _isEditingText is true then _editTitleTextField return text field. Textfield has parameter onSubmitted, so in onSubmitted method new value is assigned to initialText which is the updated value getting from _editingController.
Tada! This way we make the text editable and update the value of the text!

This can be done by having a StatefulWidget that keeps track of the state, by using a boolean for example. Let's say this bool is named editing. When editing == false you display a Text widget with the text to display, wrapped within a handler for the tap, such as a GestureDetector. When the user taps, the state is changed to editing = true. In this state you'll display a TextField with this value. You can use a TextEditingController within in this StatefulWidget and update its value there. This would allow you to up have all text selected when the user taps and changes to the editing state

I believe this is the correct answer:
class InlineEditableText extends StatefulWidget {
const InlineEditableText({
Key? key,
required this.text,
required this.style,
}) : super(key: key);
final String text;
final TextStyle style;
#override
State<InlineEditableText> createState() => _InlineEditableTextState();
}
class _InlineEditableTextState extends State<InlineEditableText> {
var _isEditing = false;
final _focusNode = FocusNode();
late String _text = widget.text;
late final TextStyle _style = widget.style;
late TextEditingController _controller;
#override
void initState() {
_controller = TextEditingController(text: _text);
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
setState(() => _isEditing = false);
} else {
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.value.text.runes.length,
);
}
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: () => setState(() {
_isEditing = !_isEditing;
_focusNode.requestFocus();
}),
child: TextField(
maxLines: 1,
style: _style,
focusNode: _focusNode,
controller: _controller,
onSubmitted: (changed) {
setState(() {
_text = changed;
_isEditing = false;
});
},
showCursor: _isEditing,
cursorColor: Colors.black,
enableInteractiveSelection: _isEditing,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 0,
vertical: 4.4,
),
border: _isEditing
? const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(0)),
)
: InputBorder.none,
),
),
);
}
}

Related

how to focus the next text field automatically after user input 1 character in flutter

I have 4 textFormField widgets. Once the user has completed the first text field I would like to focus on the next textField automatically. Is there a way to do this in Flutter? anyone please share , thank in advance :)
This can be done in Flutter in different ways, and I'll try to share the simplest one of them. Before getting into the answer, it's worth mentioning the following issue:
Detect when delete is typed into a TextField #14809
In Flutter, backspace does not send any event when the TextField is empty (i.e. TextField.onChanged won't be called). In your case, if the user is at third field and they press backspace to return to the second field, there's no way to capture that key press without some workaround that were discussed in the linked issue. In short, you'll need to add a zero-width space character (it doesn't get rendered but is present in the String) to detect backspace events.
I mentioned this issue because I'm sharing an example that utilize the zero-width space character (zwsp for short).
In the following example, I simply created two lists that contains:
FocusNode for each field
TextEditingController for each field.
Based on the index, you can bring the focus to a specific field by calling:
FocusNode.requestFocus().
Similarly, you can remove the focus by calling FocusNode.unfocus or you can remove any focus from anywhere by calling: FocusScope.of(context).unfocus(); (in the example below, it's used after the last character is inserted to hide the keyboard).
That being said, here's a full example that you can copy and paste to try it out:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(child: CodeField()),
);
}
}
/// zero-width space character
///
/// this character can be added to a string to detect backspace.
/// The value, from its name, has a zero-width so it's not rendered
/// in the screen but it'll be present in the String.
///
/// The main reason this value is used because in Flutter mobile,
/// backspace is not detected when there's nothing to delete.
const zwsp = '\u200b';
// the selection is at offset 1 so any character is inserted after it.
const zwspEditingValue = TextEditingValue(text: zwsp, selection: TextSelection(baseOffset: 1, extentOffset: 1));
class CodeField extends StatefulWidget {
const CodeField({Key key}) : super(key: key);
#override
_CodeFieldState createState() => _CodeFieldState();
}
class _CodeFieldState extends State<CodeField> {
List<String> code = ['', '', '', ''];
List<TextEditingController> controllers;
List<FocusNode> focusNodes;
#override
void initState() {
// TODO: implement initState
super.initState();
focusNodes = List.generate(4, (index) => FocusNode());
controllers = List.generate(4, (index) {
final ctrl = TextEditingController();
ctrl.value = zwspEditingValue;
return ctrl;
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// give the focus to the first node.
focusNodes[0].requestFocus();
});
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
focusNodes.forEach((focusNode) {
focusNode.dispose();
});
controllers.forEach((controller) {
controller.dispose();
});
}
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4,
(index) {
return Container(
width: 20,
height: 20,
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
child: TextField(
controller: controllers[index],
focusNode: focusNodes[index],
maxLength: 2,
keyboardType: TextInputType.number,
decoration: InputDecoration(
counterText: "",
),
onChanged: (value) {
if (value.length > 1) {
// this is a new character event
if (index + 1 == focusNodes.length) {
// do something after the last character was inserted
FocusScope.of(context).unfocus();
} else {
// move to the next field
focusNodes[index + 1].requestFocus();
}
} else {
// this is backspace event
// reset the controller
controllers[index].value = zwspEditingValue;
if (index == 0) {
// do something if backspace was pressed at the first field
} else {
// go back to previous field
controllers[index - 1].value = zwspEditingValue;
focusNodes[index - 1].requestFocus();
}
}
// make sure to remove the zwsp character
code[index] = value.replaceAll(zwsp, '');
print('current code = $code');
},
),
);
},
),
);
}
}
You may want to use a FocusNode on each of your TextFormField, this way, once your user has enter text in the TextFormField, you can use in the callback onChanged of the TextFormField call myNextTextFieldFocusNode.requestFocus()
FocusNode textFieldOne = FocusNode();
FocusNode textFieldTwo = FocusNode();
// ...
TextFormField(
onChanged: (_) {
textFieldTwo.requestFocus();
},
focusNode: textFieldOne,
controller: textController,
)
You can use onChanged and nodefocus properties. When onchanged called refer to next textfield.
init a focus node ;
late FocusNode myFocusNode;
#override
void initState() {
super.initState();
myFocusNode = FocusNode();
}
#override
void dispose() {
// Clean up the focus node when the Form is disposed.
myFocusNode.dispose();
super.dispose();
}
onChanged property;
TextField(
focusNode: myFocusNode1,
onChanged: (text) {
myFocusNode2.requestFocus();// I could not remember the correct usage please check
},
),

Flutter/Dart - How to Save State when Navigating back to a Form with Textfields?

Currently, when a user fills in a TextField the text is lost if they navigate away and then return. How can I get the text to stay in the field upon their return?
Here's the stateful widget I'm using;
class _EditPageState extends State<EditPage> {
final _formKey = GlobalKey<FormState>();
String audiotitle;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
child: Container(
child: TextField(
decoration: new InputDecoration(
hintText: widget.oldaudiotitle,
),
keyboardType: TextInputType.text,
onChanged: (titleText) {
setState(() {
this.audiotitle = titleText;
});
},
),
),
),
);
}
}
What am I doing wrong here?
you have two ways :
store the Data of the text field and set the data in init method
(use sharedpreferences or any other database as per your requirements)
TextEditingController controller = TextEditingController();
#override
void initState() {
// TODO: implement initState
// retrive the Data
if(data != null) {
controller = new TextEditingController(text: data);
}
}
or if the first screen is navigating in the second Screen than just pop that screen
Navigator.pop(context);

How can I correctly focus a Textfield that is conditionally created based on the focus node's hasFocus value?

With my current code the TextField becomes focused, but the cursor and keyboard aren't triggered (requires a second tap). I believe this is because the TextField doesn't exist when the focus node is initially focused, but I'm struggling to find a solution.
Here is a simple recreation of the problem based on a Cookbook recipe:
class MyCustomForm extends StatefulWidget {
#override
_MyCustomFormState createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
FocusNode myFocusNode;
bool _editingField2 = false;
#override
void initState() {
super.initState();
myFocusNode = FocusNode();
myFocusNode.addListener(_focusListener);
}
#override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
// Set _editingField2 to true when focusNode has focus.
_focusListener() {
if (myFocusNode.hasFocus) {
setState(() {
_editingField2 = true;
});
} else {
setState(() {
_editingField2 = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Text Field Focus'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// The first text field is focused on as soon as the app starts.
TextField(
autofocus: true,
),
// The second text field is created when _editingField2 (after FAB press).
_editingField2
? TextField(
focusNode: myFocusNode,
)
: Text('ayy'),
],
),
),
floatingActionButton: FloatingActionButton(
// Give focus node focus on FAB press.
onPressed: () => FocusScope.of(context).requestFocus(myFocusNode),
tooltip: 'Focus Second Text Field',
child: Icon(Icons.edit),
),
);
}
}
Here is my code, with important bits commented.
class TaskListItem extends StatefulWidget {
final Task task;
TaskListItem({#required this.task});
#override
State createState() => _TaskListItemState();
}
class _TaskListItemState extends State<TaskListItem> {
bool _isEditing;
FocusNode _focusNode;
final TextEditingController _textEditingController = TextEditingController();
#override
initState() {
super.initState();
_isEditing = false;
_textEditingController.text = widget.task.text;
_textEditingController.addListener(_handleTextFieldUpdate);
_focusNode = FocusNode(debugLabel: 'TaskListItem');
_focusNode.addListener(_handleFocusChange);
}
#override
void dispose() {
_focusNode.removeListener(_handleFocusChange);
_focusNode.dispose();
_textEditingController.dispose();
super.dispose();
}
_handleTextFieldUpdate() {
Provider.of<TaskListModel>(context, listen: false)
.updateTaskText(widget.task, _textEditingController.text);
}
// Update state to determine if Text or TextField widget is created in build().
_handleFocusChange() {
if (_focusNode.hasFocus) {
setState(() {
_isEditing = true;
});
} else {
setState(() {
_isEditing = false;
});
}
}
Widget _buildTitle() {
return Row(
children: <Widget>[
Expanded(
// Create either TextField or Text based on _isEditing value.
child: _isEditing && !widget.task.isComplete
? TextField(
focusNode: _focusNode,
controller: _textEditingController,
)
: Text(
widget.task.text,
style: widget.task.isComplete
? TextStyle(decoration: TextDecoration.lineThrough)
: null,
),
),
],
);
}
#override
Widget build(BuildContext context) {
return ListTile(
leading: Checkbox(
value: widget.task.isComplete,
//Dismiss focus when box is checked
onChanged: (bool checked) {
_focusNode.unfocus();
Provider.of<TaskListModel>(context, listen: false)
.toggleComplete(widget.task);
},
),
title: _buildTitle(),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => Provider.of<TaskListModel>(context, listen: false)
.deleteTask(widget.task),
),
onTap: () {
// I'm requesting focus here, but the Textfield doesn't exist yet?
FocusScope.of(context).requestFocus(_focusNode);
print('tapped');
},
);
}
}
What you have to do is change focus inside build, you're trying to change focus before the screen has done rebuilding that widget already. try this please, using your own code.
I'm not sure if you needed to really listen to that focus change or if you only wanted to accomplish the focus change after enabling the widget, if you do want to listen for the focus change let me know.
class MyCustomForm extends StatefulWidget {
#override
_MyCustomFormState createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
FocusNode myFocusNode = FocusNode();
bool _editingField2 = false;
#override
void dispose() {
myFocusNode?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
//here you do the focus request
if (_editingField2) {
FocusScope.of(context).requestFocus(myFocusNode);
}
return Scaffold(
appBar: AppBar(
title: Text('Text Field Focus'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// The first text field is focused on as soon as the app starts.
TextField(
autofocus: true,
),
// The second text field is created when _editingField2 (after FAB press).
_editingField2
? TextField(
focusNode: myFocusNode,
)
: Text('ayy'),
],
),
),
floatingActionButton: FloatingActionButton(
// Give focus node focus on FAB press.
onPressed: () {
setState(() {
_editingField2 = true;
});
},
tooltip: 'Focus Second Text Field',
child: Icon(Icons.edit),
),
);
}
}

Is it possible to position the error message below a TextFormField?

I need to fully customize the rendering of input fields and especially the way error messages are rendered. I need them to be displayed below the field without any other modification on the field.
Right now either the field is resized (default) or fixed when I set helperText: ' ' as stated in the doc, but then the field height becomes huge to keep space for the error message.
Of course I can add a Text widget below my field for the error (that's what I did actually) but then I can't use form validation as it relies on the built-in error management of the input widget to display error messages... So this would require to re-create my own form validation.
One thing I've been thinking of would be to prevent the form validation to display the error message but I couldn't find a way to do it.
Any idea?
Edit : some code bellow :
class CustomTextFormField extends StatefulWidget {
final FormFieldSetter<String> onSaved;
final ValueChanged<String> onFieldSubmitted;
final TextInputAction textInputAction;
final String initialValue;
final bool autofocus;
final bool obscureText;
final String label;
final bool withDivider;
final FocusNode focusNode;
final bool required;
CustomTextFormField(
{Key key,
this.onSaved,
this.onFieldSubmitted,
this.textInputAction,
this.initialValue,
this.autofocus = false,
this.obscureText = false,
this.label,
this.withDivider = false,
this.focusNode,
this.required})
: super(key: key);
CustomTextFormFieldState createState() => CustomTextFormFieldState();
}
class CustomTextFormFieldState extends State<CustomTextFormField> with CustomFormFieldState {
bool _isMasked = false;
String _showObscureIcon;
FocusNode _focusNode;
TextEditingController _controller;
String _errorText;
/// True if this field has any validation errors.
bool get hasError => _errorText != null;
#override
void initState() {
// Add a listener to the focusNode to detect when we loose focus
_focusNode = widget.focusNode != null ? widget.focusNode : FocusNode();
_focusNode.addListener(lostFocusListener);
// Create the TextEditingController for the field with optional initial value
_controller = TextEditingController(text: widget.initialValue);
_controller.addListener(() {
CustomForm.of(context).fieldDidChange(); // CustomForm is basically a simplified and slightly adapted version of the built-in Form
});
// Set obscure properties
if (widget.obscureText) {
_isMasked = true;
_showObscureIcon = 'assets/img/icon_displaypassword.png';
}
super.initState();
}
/// Displays the field label
Widget _inputLabel() {
return Container(
child: Text(widget.label,
style: hasError ? TextStyle(color: themeErrorColor) : null),
margin: const EdgeInsets.only(bottom: 8.0),
);
}
/// Displays the field
Widget _inputField() {
return Expanded(
child: TextFormField(
decoration: InputDecoration(
enabledBorder: themeInputBorder,
focusedBorder: themeInputBorder,
contentPadding: EdgeInsets.all(14.0),
),
textInputAction: widget.textInputAction,
autofocus: widget.autofocus,
onSaved: widget.onSaved,
onFieldSubmitted: widget.onFieldSubmitted,
obscureText: _isMasked,
focusNode: _focusNode,
controller: _controller,
));
}
/// Displays the obscured toggle button
Widget _obscureButton() {
return IconButton(
icon: Image.asset(_showObscureIcon),
onPressed: _toggleObscureText,
);
}
/// Displays the decorated field
Widget _decoratedField() {
// Row that contains the input field
List<Widget> rowChildren = <Widget>[_inputField()];
// If the field must be oscured, we add the toggle button to the row
if (widget.obscureText) {
rowChildren.add(_obscureButton());
}
return Container(
decoration: hasError ? themeInputErrorDecoration : themeInputDecoration, // themeInputErrorDecoration and themeInputDecoration are LinearGradient defined elsewhere
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(2.0)),
child: Row(
children: rowChildren,
),
));
}
/// Displays the error message if any
Widget _errorLabel() {
return Offstage(
offstage: !hasError,
child: Container(
margin: EdgeInsets.only(top: 10.0),
child: Text(_errorText == null ? '' : _errorText,
style: TextStyle(color: themeErrorColor))));
}
/// Toggles beetween obscured/clear text and action icon
void _toggleObscureText() {
setState(() {
_isMasked = !_isMasked;
_showObscureIcon = _isMasked
? 'assets/img/icon_displaypassword.png'
: 'assets/img/icon_dontdisplaypassword.png';
});
}
/// Callback called when the focus has changed. Validates the input text
void lostFocusListener() {
if (!_focusNode.hasFocus) {
if (!touched) {
touched = true;
}
}
}
/// Saves the field
void save() {
widget.onSaved(_controller.text);
}
/// Resets the field to its initial value.
void reset() {
setState(() {
_controller.text = widget.initialValue;
_errorText = null;
});
}
/// Validates the field and set the [_errorText]. Returns true if there
/// were no errors.
bool validate() {
// If the field has not been touched yet, the field is validated
if(!touched) {
return true;
}
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
// TODO: write a real validator
if (_controller.text.isEmpty && widget.required) {
_errorText = 'Field is required';
} else {
_errorText = null;
}
}
#override
Widget build(BuildContext context) {
register(context);
// Widgets for the column
List<Widget> children = <Widget>[
_inputLabel(),
_decoratedField(),
_errorLabel()
];
// Adds a divider if neeed to the column
if (widget.withDivider) {
children.add(Divider(height: 40.0));
}
// Renders the column
return Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children);
}
#override
void dispose() {
// Clean up the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
#override
void deactivate() {
unregister(context);
super.deactivate();
}
}
abstract class CustomFormFieldState {
String errorText;
/// True if this field has any validation errors.
bool get hasError => errorText != null;
bool touched = false;
void register(BuildContext context) {
CustomForm.of(context)?.register(this);
}
void unregister(BuildContext context) {
CustomForm.of(context)?.unregister(this);
}
void save();
void reset();
bool validate();
}

Flutter How to always hide keyboard when click on TextField but keep focus(Keep show cursor)

I can't control keyboard show or hide, In my project I need to always hide keyboard but keep focused to display my custom keyboard(a widget).
This is I want
And this is my problem
To hide keyboard and keep the cursor visible, set readOnly to true and showCursor to true.
TextFormField(
showCursor: true,
readOnly: true),
See flutter/issues/#16863
FYI, TextInputType.none was introduced in #83974:
TextField(
keyboardType: TextInputType.none,
...
)
You can use custom focusNode
This prevents keyboard appearing only on first tap:
TextField(focusNode: FirstDisabledFocusNode(),)
class FirstDisabledFocusNode extends FocusNode {
#override
bool consumeKeyboardToken() {
return false;
}
}
This prevents always:
TextField(focusNode: AlwaysDisabledFocusNode())
class AlwaysDisabledFocusNode extends FocusNode {
#override
bool get hasFocus => false;
}
Insert NoKeyboardEditableText instead your TextField
class NoKeyboardEditableText extends EditableText {
NoKeyboardEditableText({
#required TextEditingController controller,
TextStyle style = const TextStyle(),
Color cursorColor = Colors.black,
bool autofocus = false,
Color selectionColor
}):super(
controller: controller,
focusNode: NoKeyboardEditableTextFocusNode(),
style: style,
cursorColor: cursorColor,
autofocus: autofocus,
selectionColor: selectionColor,
backgroundCursorColor: Colors.black
);
#override
EditableTextState createState() {
return NoKeyboardEditableTextState();
}
}
class NoKeyboardEditableTextState extends EditableTextState {
#override
Widget build(BuildContext context) {
Widget widget = super.build(context);
return Container(
decoration: UnderlineTabIndicator(borderSide: BorderSide(color: Colors.blueGrey)),
child: widget,
);
}
#override
void requestKeyboard() {
super.requestKeyboard();
//hide keyboard
SystemChannels.textInput.invokeMethod('TextInput.hide');
}
}
class NoKeyboardEditableTextFocusNode extends FocusNode {
#override
bool consumeKeyboardToken() {
// prevents keyboard from showing on first focus
return false;
}
}
try use input_with_keyboard_control package
It helped me solve my problem, of receiving the text from a barcode scanner, without showing the keyboard