Saturday, May 30, 2020

Flutter - Formatting TextField with TextInputFormatter

To take valid formatted data from the end-user is an essential task when your application saying to enter detail about credit cards, phone numbers, zip code, etc. This approach facilitates the developer to entering valid data in the application. It makes the process easier,  gives feedback, minimizes errors, and generally improves the user experience.

flutter qr and barcode demo
In Flutter, we can take valid formatted data with the help of TextInputFormatter widget that's provides us various different constraints. The inputFormatters property of TextFormField and TextField allows the developers to pass a list of TextInputFormatter widgets to define how that field will behave. A TextInputFormatter can be optionally inject into a TextFormField and TextField that works as a validator and correct the format of text when the text being edited. The TextInputFormatter translates the field's value into the text and the text into the field's value.

In this post, you’ll learn how to format the data in a TextFormField and TextField. When a user types something into an input field. The value will be adjusted automatically and add things like punctuation and dashes, removing unexpected characters, trimming spaces, and changing the word-casing.


Type of TextInputFormatter

First of all, let's understand all the available TextInputFormatter in Flutter. The Flutter framework gives us 3 useful built-in TextInputFormatter.
  • LengthLimitingTextInputFormatter: It prevents the user to insertion of more characters than a limit. You can use it if you want define max length of input field.
  • WhitelistingTextInputFormatter: The user can insert only whitelisted characters in the input field like you can use WhitelistingTextInputFormatter.digitsOnly that takes digits [0–9] only.
  • BlacklistingTextInputFormatter: It prevents the insertion of blacklisted characters in the input field like BlacklistingTextInputFormatter.singleLineFormatter forces user to enter characters a single line.
You can use RegExp as well to customize your formatters like:

WhitelistingTextInputFormatter(RegExp("[a-zA-Z]"))
BlacklistingTextInputFormatter(RegExp("[/\\\\]"))





How can use TextInputFormatter

You can use TextInputFormatter in inputFormatters param of TextField as explained below:

 TextFormField(
inputFormatters: <TextInputFormatter>[
LengthLimitingTextInputFormatter(12),
],
)

//You can combine formatters
TextFormField(
inputFormatters: <TextInputFormatter>[
LengthLimitingTextInputFormatter(12),
WhitelistingTextInputFormatter.digitsOnly,
BlacklistingTextInputFormatter.singleLineFormatter,

],
)


customize TextInputFormatter

As you have seen examples of TextInputFormatter to use in Widget tree. But some times you need create your own formatter to acheive some specific task. We can create a customized TextInputFormatter by extending the TextInputFormatter and implement formatEditUpdate method.

  1. The following code will let you specify a mask and a separator string to implement the required behavior.

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';

    class CardFormatter extends TextInputFormatter {
    final String mask;
    final String separator;

    CardFormatter({
    @required this.mask,
    @required this.separator,
    }) {
    assert(mask != null);
    assert(separator != null);
    }

    @override
    TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    if (newValue.text.length > 0) {
    if (newValue.text.length > oldValue.text.length) {
    if (newValue.text.length > mask.length) return oldValue;
    if (newValue.text.length < mask.length && mask[newValue.text.length - 1] == separator) {
    return TextEditingValue(
    text:
    '${oldValue.text}$separator${newValue.text.substring(newValue.text.length - 1)}',
    selection: TextSelection.collapsed(
    offset: newValue.selection.end + 1,
    ),
    );
    }
    }
    }
    return newValue;
    }
    }


    You can above customized TextInputFormatter:

    TextField(
    inputFormatters: [
    MaskedTextInputFormatter(
    mask: 'xxxx-xxxx-xxxx-xxxx',
    separator: '-',
    ),
    ],
    );

    As we have passed mask “xxxx-xxxx-xxxx-xxxx” and the separator “-”  to manage credit card behavior.
  2. Here, we going to explain another customized TextInputFormatter. With the help of following TextInputFormatter , you can navigate to target Textfield with in the widget tree of screen. 

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';

    class TextFieldNavigator extends TextInputFormatter {
    final FocusNode focusNodeNext;
    final FocusNode focusNodePrev;
    final BuildContext context;

    TextFieldNavigator({
    @required this.context,
    @required this.focusNodeNext,
    @required this.focusNodePrev,
    }) {
    assert(context != null);

    assert(focusNodeNext != null);
    assert(focusNodePrev != null);
    }

    @override
    TextEditingValue formatEditUpdate(
    TextEditingValue oldValue, TextEditingValue newValue) {
    if (oldValue.text.length == 3 && newValue.text.length == 4) {
    FocusScope.of(context).requestFocus(focusNodeNext);
    } else if (oldValue.text.length == 1 && newValue.text.length == 0) {
    FocusScope.of(context).requestFocus(focusNodePrev);
    }
    return newValue;
    }
    }


Here, example of above customized and built in TextInputFormatter:

Flutter TextInputFormatter  example




Here, you can see complete code snippet of above demo where we have used above explained custom
TextInputFormatter.

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:fluttercardscreen/utils/TextFieldNavigator.dart';
import 'package:fluttercardscreen/utils/card_formatter.dart';
import 'package:fluttercardscreen/utils/colors_constant.dart';
import 'package:fluttercardscreen/utils/string_util.dart';
import 'package:fluttercardscreen/widgets/custom_text.dart';

void main() => runApp(CardScreenWidget());


class CardScreenWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primaryColor: const Color(0xFF02BB9F),
primaryColorDark: const Color(0xFF167F67),
accentColor: const Color(0xFF167F67),
),
home: CardScreenPage(title: 'Flutter Demo Home Page'),
);
}
}

class CardScreenPage extends StatefulWidget {
CardScreenPage({Key key, this.title}) : super(key: key);
final String title;

@override
_CardScreenPageState createState() => _CardScreenPageState();
}

class _CardScreenPageState extends State<CardScreenPage> {
final _formKey = GlobalKey<FormState>();

final _teOtpDigitOne = TextEditingController();
final _teOtpDigitTwo = TextEditingController();
final _teOtpDigitThree = TextEditingController();
final _teOtpDigitFour = TextEditingController();

FocusNode _focusNodeDigitOne = new FocusNode();
FocusNode _focusNodeDigitTwo = new FocusNode();
FocusNode _focusNodeDigitThree = new FocusNode();
FocusNode _focusNodeDigitFour = new FocusNode();

final _teDob = TextEditingController();
FocusNode _focusNodeDob = FocusNode();

final _teExpireDate = TextEditingController();
FocusNode _focusNodeExpireDate = FocusNode();

final _teCard = TextEditingController();
FocusNode _focusNodeCard = FocusNode();

void _submit() {
final form = _formKey.currentState;
if (form.validate()) {
form.save();
FocusScope.of(context).requestFocus(FocusNode());
}
}

@override
Widget build(BuildContext context) {
var loginForm = Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 15.0),
child: CustomText(
label: StringUtil.CARD_NUMBER,
fontSize: 13.0,
fontName: "pp_medium",
textAlign: TextAlign.right,
textColor: ColorConstants.BLACK,
).text(),
),
Row(
children: <Widget>[
Expanded(
child: inputFiledNew(
_teOtpDigitOne,
_focusNodeDigitOne,
"",
TextInputType.number,
StringUtil.NUMBER,
[
TextFieldNavigator(context: context,
focusNodeNext: _focusNodeDigitTwo,
focusNodePrev: _focusNodeDigitOne),
LengthLimitingTextInputFormatter(4)
],
false,
TextAlign.center),
flex: 1,
),
SizedBox(
width: 10.0,
),
Expanded(
child: inputFiledNew(
_teOtpDigitTwo,
_focusNodeDigitTwo,
"",
TextInputType.number,
StringUtil.NUMBER,
[
TextFieldNavigator(context: context,
focusNodeNext: _focusNodeDigitThree,
focusNodePrev: _focusNodeDigitOne),
LengthLimitingTextInputFormatter(4)
],
false,
TextAlign.center),
flex: 1,
),
SizedBox(
width: 10.0,
),
Expanded(
child: inputFiledNew(
_teOtpDigitThree,
_focusNodeDigitThree,
"",
TextInputType.number,
StringUtil.NUMBER,
[
TextFieldNavigator(context: context,
focusNodeNext: _focusNodeDigitFour,
focusNodePrev: _focusNodeDigitTwo),
LengthLimitingTextInputFormatter(4)
],
false,
TextAlign.center),
flex: 1,
),
SizedBox(
width: 10.0,
),
Expanded(
child: inputFiledNew(
_teOtpDigitFour,
_focusNodeDigitFour,
"",
TextInputType.number,
StringUtil.NUMBER,
[
TextFieldNavigator(context: context,
focusNodeNext: _focusNodeDigitFour,
focusNodePrev: _focusNodeDigitThree),
LengthLimitingTextInputFormatter(4)
],
false,
TextAlign.center),
flex: 1,
),
],
),

SizedBox(height: 40.0,),
inputFiledNew(
_teCard,
_focusNodeCard,
StringUtil.CARD_NUMBER,
TextInputType.number,
StringUtil.CARD_FORMAT,
[
CardFormatter(
mask: 'xxxx-xxxx-xxxx-xxxx',
separator: '-',
),
],
false,
TextAlign.left
),


Padding(
padding: const EdgeInsets.only(top: 40.0),
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 15.0),
child: CustomText(
label: StringUtil.DOB,
fontSize: 13.0,
fontName: "pp_medium",
textAlign: TextAlign.right,
textColor: ColorConstants.BLACK,
).text(),
),
inputFiledNew(
_teDob,
_focusNodeDob,
StringUtil.PLEASE_ENTER_DOB,
TextInputType.number,
StringUtil.DOB_FORMAT,
[
CardFormatter(
mask: 'xx.xx.xx',
separator: '.',
),
],
false,
TextAlign.center),
],
),
flex: 1,
),
SizedBox(
width: 20.0,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 15.0),
child: CustomText(
label: StringUtil.EXPIRY_DATE,
fontSize: 13.0,
fontName: "pp_medium",
textAlign: TextAlign.right,
textColor: ColorConstants.BLACK,
).text(),
),
inputFiledNew(
_teExpireDate,
_focusNodeExpireDate,
StringUtil.PLEASE_ENTER_EXPIRE_DATE,
TextInputType.number,
StringUtil.EXPIRY_DATE_FORMAT,
[
CardFormatter(
mask: 'xx/xx',
separator: '/',
),
],
false,
TextAlign.center
),
],
),
flex: 1,
)
],
),
),
GestureDetector(
onTap: () {
_submit();
},
child: Container(
margin: EdgeInsets.fromLTRB(40.0, 40.0, 40.0, 0.0),
child: buttonMedium(
StringUtil.Add_CARD,
EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
Color(0xFF167F67),
Color(0xFFFFFFFF),
16.0),
),
),
]));

return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios, color: Colors.white,),
onPressed: () {},
),
title: CustomText(
label: StringUtil.MANAGE_CARDS,
fontSize: 20.0,
fontName: "pp_medium",
textAlign: TextAlign.center,
textColor: 0xFFFFFFFF,
).text(),
),
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(40.0),
child: loginForm,
),
),
),
);
}


Widget inputFiledNew(TextEditingController teController,
FocusNode focusNode,
String errorMsg,
TextInputType inputType,
String hintMsg,
List<TextInputFormatter> cardFormatter,
bool isHide,
TextAlign textAlign) {
return TextFormField(
validator: (val) => val.isEmpty ? errorMsg : null,
onSaved: (val) => val,
controller: teController,
keyboardType: inputType,
focusNode: focusNode,
obscureText: isHide,

textAlign: textAlign,
style: TextStyle(
color: Color(ColorConstants.TITILE), fontFamily: "pp_medium",),
inputFormatters: cardFormatter,
decoration: InputDecoration(
hintText: hintMsg,
hintStyle: TextStyle(fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
width: 0,
style: BorderStyle.none,
),
),
filled: true,
contentPadding: EdgeInsets.all(10),
fillColor: Color(ColorConstants.COLOR_INPUT_FIELD),
),
);
}

Widget buttonMedium(String buttonLabel, EdgeInsets margin, Color bgColor,
Color textColor, double textSize) {
var loginBtn = Container(
margin: margin,
padding: EdgeInsets.all(10.0),
alignment: FractionalOffset.center,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.all(const Radius.circular(60.0)),
),
child: Text(
buttonLabel,
style: TextStyle(
color: textColor, fontSize: textSize, fontWeight: FontWeight.bold),
),
);
return loginBtn;
}

}

As we can see, there is no limit. Flutter offers to developers excellent built-in formatters and makes easy to extend and create our own. Let’s use. Make easy to your user input data in form fields with formatters. I hope this was useful and a good starting point so you can extend this code and implement your desired behavior

Share:

Get it on Google Play

React Native - Start Development with Typescript

React Native is a popular framework for building mobile apps for both Android and iOS. It allows developers to write JavaScript code that ca...