I was working on a simple app for practice and wanted my text fields to accept only digits and format them in Turkish currency like this "1.234,5678".
To achieve this, I created a widget derived from the TextFormField widget and used its inputFormatters property by creating a new TextInputFormatter class and overriding its formatEditUpdate method. This method works when text is being typed, cut, copied, or pasted.
When the text is edited, the order of operations is as follows:
Clean the string of incorrect characters and adjacent zeros at foremost.
Correct the decimal point positions before the comma.
Merge strings like this: [correct and right dotted string, comma, correct digits after comma]
Here are the codes with comments. I hope you find them useful. If you know better solutions or algorithms, please leave a comment.
helper.dart (Functions for use in Text Input Formatter)
/// Returns a new list excluding some elements of the original list.
List<T> listByExcludingIndexes<T>(
List<T> originalList, List<int> excludeIndices) {
final newList = <T>[];
for (int i = 0; i < originalList.length; i++) {
if (!excludeIndices.contains(i)) {
newList.add(originalList[i]);
}
}
return newList;
}
/// Returns a list sanitized from unwanted characters from given list.
/// * Non-Digit characters
/// * Commas except one
List<String> cleanedListFromIncorrects(List<String> strList) {
List<int> indexesToBeDeleted = [];
int commaCount = 0;
for (int i = 0; i < strList.length; i++) {
// If it's not a digit it'll be removed except one comma
if (int.tryParse(strList[i]) == null) {
// If there are commas more than one then it'll be removed.
if (strList[i] == ',') {
commaCount++;
if (commaCount > 1) {
indexesToBeDeleted.add(i);
}
} else {
indexesToBeDeleted.add(i);
}
}
}
return listByExcludingIndexes(strList, indexesToBeDeleted);
}
/// Clear points inside the given list.
/// Return a new list that contains points in the right places.
List<String> pointsCorrectedList(List<String> strList) {
// Clear all points
strList.removeWhere((s) => s == '.');
// There must be at least 4 digits in order to be found a dot.
if (strList.length < 4) {
return strList;
}
List<int> pointPositions = [];
// Determine point places and write inside the pointPositions list.
for (int i = strList.length; i > 0; i--) {
if (i % 3 == 0) {
pointPositions.add(strList.length - i);
} else if (i % 3 == 2 && i < 2) {
pointPositions.add(strList.length + 2 - i);
} else if (i % 3 == 1 && i < 1) {
pointPositions.add(strList.length + 3 - i);
}
}
// There shouldn't be a point in the first place.
pointPositions.removeWhere((p) => p == 0);
// Insert points inside the given list.
for (int i = 0; i < pointPositions.length; i++) {
strList.insert(pointPositions[i] + (i > 0 ? i : 0), '.');
}
// Return not a reference but a new list from strList.
return List.from(strList);
}
/// Returns a list sanitized from adjacent zeros located foremost.
List<String> cleanedListFromAdjacentZeros(List<String> strList) {
List<int> positions = [];
if (strList.length > 1) {
for (int i = 0; i < strList.length; i++) {
if (strList[i] == '0') {
positions.add(i);
} else if (strList[i] != '0') {
break;
}
}
}
if (positions.isEmpty) {
return strList;
}
final result = listByExcludingIndexes(strList, positions);
if (result.isEmpty) {
return ['0'];
}
return result;
}
/// Returns currency formatted text.
String currencyFormattedText(String str) {
// Array of the given string
var strList = str.split('');
// 1.) Clean string list from inccorrect characters and adjacent zeros.
final cleanList =
cleanedListFromAdjacentZeros(cleanedListFromIncorrects(strList));
// 2.) Determine comma position in the string list.
final commaIndex = cleanList.indexOf(',');
// 3.) Correct point positions before comma
if (commaIndex > -1) {
final cleanListBeforeComma = cleanList.getRange(0, commaIndex).toList();
final cleanListAfterComma =
cleanList.getRange(commaIndex + 1, cleanList.length).toList();
final pointsCorrectedListBeforeComma =
pointsCorrectedList(cleanListBeforeComma);
strList = [...pointsCorrectedListBeforeComma, ',', ...cleanListAfterComma];
} else {
final pcl = pointsCorrectedList(cleanList);
if (pcl.isEmpty) {
strList = ['0', ',', '0', '0'];
} else {
strList = [...pcl, ',', '0', '0'];
}
}
if (strList.length == 1 && strList[0] == ',') {
strList = ['0', ',', '0', '0'];
}
if (strList[0] == ',') {
strList.insert(0, '0');
}
return strList.join();
}
/// Currency formatted string to double
double currencyToDouble(String cur) {
var str = cur.replaceAll('.', '');
str = str.replaceFirst(',', '.');
var strToDouble = double.tryParse(str);
if (strToDouble != null) {
return strToDouble;
}
return 0;
}
currency_form_field.dart (The Widget)
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:kkasgariyathesap/widgets/currency_form_field/helper.dart';
class CurrencyFormField extends TextFormField {
final double? initialAmount;
final Function(double) onChangedAmount;
final String? Function(double?)? validateAmount;
CurrencyFormField({
super.key,
required this.onChangedAmount,
this.validateAmount,
this.initialAmount = 0,
super.autofocus,
super.style,
}) : super(
decoration: const InputDecoration(
suffix: Text('₺'),
),
initialValue: currencyFormattedText('$initialAmount'),
keyboardType: TextInputType.number,
textAlign: TextAlign.right,
inputFormatters: [CurrencyInputFormatter()],
onChanged: (value) {
onChangedAmount(currencyToDouble(value));
},
validator: validateAmount == null
? null
: (value) {
return value == null
? validateAmount(null)
: validateAmount(currencyToDouble(value));
});
}
class CurrencyInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
var selection = newValue.selection;
if (newValue.text.isNotEmpty) {
var formattedText = currencyFormattedText(newValue.text);
// Move cursor right when the point is made.
var pointCount = formattedText.split('').where((e) => e == '.').length;
var oldPointCount = oldValue.text.split('').where((e) => e == '.').length;
if (pointCount > oldPointCount) {
selection = TextSelection.collapsed(offset: selection.end + 1);
}
// Select zero if there's only zero left of the comma
if ((selection.end == 0 || selection.end == 1) &&
formattedText[0] == '0') {
selection = const TextSelection(baseOffset: 0, extentOffset: 1);
}
return TextEditingValue(
text: formattedText,
selection: selection,
);
}
return newValue;
}
}
new_page.dart (Main page)
import 'package:flutter/material.dart';
import 'package:kkasgariyathesap/widgets/currency_form_field/currency_form_field.dart';
class NewPage extends StatefulWidget {
const NewPage({super.key});
@override
State<NewPage> createState() => _NewPageState();
}
class _NewPageState extends State<NewPage> {
double _amount = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Currency Form Field Test Page'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
children: [
CurrencyFormField(onChangedAmount: (amount) {
setState(() {
_amount = amount;
});
}),
const SizedBox(height: 10),
Text('$_amount',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 20)),
],
),
),
),
);
}
}