'Flutter TextField input validation for a date
I am trying to write a date input control which accepts a date like 23/12/1997. What I would like it to do is automatically insert the / characters for the user. So as they type in 23 the listener returns 23/, so that they can then type in 12. At this point the listener again adds a / leaving the user to complete the date by typing 1997. My TextEditingController code half works and looks like this:
final _controller = TextEditingController();
_controller.addListener(() {
String text = _controller.text;
if (text.length == 2) {
text += '/';
}
if (text.length == 5) {
text += '/';
}
_controller.value = _controller.value.copyWith(
text: text,
selection:
TextSelection(baseOffset: text.length, extentOffset: text.length),
composing: TextRange.empty,
);
print(_controller.text);
}
So it works fine until the user makes a mistake and needs to backtrack. As soon as a / is deleted it is immediately replaced stopping any further editing of the date.
In order to get it to work I need to access is the previously entered text to determine if the user is backspacing. So if text == 23/ && previous_text == 23/1 then I can remove the / from text.
I found this question textfield must only accept numbers and I think it may help me, but I am not sure how to implement an existing widget and override its methods. Of course there may be a simpler way to do this within the TextEditingController?
Solution 1:[1]
You can use datepicker dialog made by flutter.
DateTime _date = DateTime.now()
onPressed: () {
showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime(2020),
lastDate: DateTime(2021),
).then((date) {
setState(() {
_date = date;
});
});
},
Solution 2:[2]
I find your code to be a great utility, without any dependencies. I took the liberty to do a few mods and thought of posting it back here, as I find your concept very neat and lightweight on the UI. The requirements were;
Validating the date for non-31-day months and leap years. The mods were quite straightforward.
Preventing the user entering "/" at undesirable places which will throw the algorithm off-track. The simplest solution is to make the
keyboardType: TextInputType.number, in the TextField
This works perfectly for mobile devices. But Flutter being cross-platform, this solution may not be foolproof when it comes to a device with physical keyboard. I tried various checks and blocks but only partially succeeded; i.e, user can still input "/" in between the digits of the Day and Month. I think there is an internal delay between KB input and programmatic formatter.
Following is the modified code for _DateFormatter. I have used the /// notion to distinguish my comments. They should be read along with the original // comments.
class _DateFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue prevText, TextEditingValue currText) {
int selectionIndex;
String date;
String month;
int year;
// Get the previous and current input strings
String pText = prevText.text;
String cText = currText.text;
cText = cText.replaceAll("//", "/");
// Abbreviate lengths
int cLen = cText.length;
int pLen = pText.length;
/// ENTERING THE DATE
if (cLen == 1) {
/// User enters the first digit of the date. The first digit
// Can only be 0, 1, 2 or 3
if (int.parse(cText) > 3) {
// Remove char
cText = '';
}
} else if (cLen == 2 && pLen == 1) {
/// User has already entered a valid first digit of the date, now he
/// enters the second digit of the date; but
// Days cannot be greater than 31
int dd = int.parse(cText.substring(0, 2));
if (dd == 0 || dd > 31) {
// Remove char
cText = cText.substring(0, 1);
} else {
/// User has entered a valid date (between 1 and 31). So now,
// Add a / char
cText += '/';
}
/// ENTERING THE MONTH
} else if (cLen == 4) {
/// after entering a valid date and programmatic insertion of '/', now User has entered
/// the first digit of the Month. But, it
// Can only be 0 or 1
/// (and, not '/' either)
if (int.parse(cText.substring(3, 4)) > 1 || cText.substring(3, 4) == "/") {
// Remove char
cText = cText.substring(0, 3);
}
} else if (cLen == 5 && pLen == 4) {
int mm = int.parse(cText.substring(3, 5));
int dd = int.parse(cText.substring(0, 2));
/// User has entered the second digit of the Month, but the
// Month cannot be greater than 12
/// Also, that entry cannot be '/'
if ((mm == 0 || mm > 12|| cText.substring(3, 5) == "/") ||
/// If the date is 31, the month cannot be Apr, Jun, Sept or Nov
(dd == 31 && (mm == 02 || mm == 04 || mm == 06 || mm == 09 || mm == 11)) ||
/// If the date is greater than 29, the month cannot be Feb
/// (Leap years will be dealt with, when user enters the Year)
(dd > 29 && (mm == 02))) {
// Remove char
cText = cText.substring(0, 4);
}
else if (cText.length == 5) {
/// the Month entered is valid; so,
// Add a / char
cText += '/';
}
} else if ((cLen == 3 && pLen == 4) || (cLen == 6 && pLen == 7)) {
// Remove / char
cText = cText.substring(0, cText.length - 1);
} else if (cLen == 3 && pLen == 2) {
if (int.parse(cText.substring(2, 3)) > 1) {
// Replace char
cText = cText.substring(0, 2) + '/';
} else {
// Insert / char
cText =
cText.substring(0, pLen) + '/' + cText.substring(pLen, pLen + 1);
}
/// ENTERING THE YEAR
} else if (cLen == 6 && pLen == 5) {
// Can only be 1 or 2 - if so insert a / char
int y1 = int.parse(cText.substring(5, 6));
if (y1 < 1 || y1 > 2) {
// Replace char
/// i.e, add '/' after the 5th position
cText = cText.substring(0, 5) + '/';
} else {
// Insert / char
cText = cText.substring(0, 5) + '/' + cText.substring(5, 6);
}
} else if (cLen == 7) {
/// the first digit of year
// Can only be 1 or 2
int y1 = int.parse(cText.substring(6, 7));
if (y1 < 1 || y1 > 2) {
// Remove char
cText = cText.substring(0, 6);
}
} else if (cLen == 8) {
// Can only be 19 or 20
/// Also, there cannot be / typed by the user
String y2 = cText.substring(6, 8);
if (y2 != "19" && y2 != "20") {
// Remove char
cText = cText.substring(0, 7);
}
} else if (cLen == 9) {
/// There cannot be / typed by the user
if (cText.substring(8, 9) == "/") {
// Remove char
cText = cText.substring(0, 8);
}
} else if (cLen == 10) {
/// There cannot be / typed by the user
if (cText.substring(9, 10) == "/") {
// Remove char
cText = cText.substring(0, 9);
}
/// If the year entered is not a leap year but the date entered is February 29,
/// it will be advanced to the next valid date
date = cText.substring(0, 2);
month = cText.substring(3, 5);
year = int.parse(cText.substring(6, 10));
bool isNotLeapYear = !((year % 4 == 0) && (year % 100 != 0) ||
(year % 400 == 0));
if (isNotLeapYear && month == "02" && date == "29") {
cText = "01/03/$year";
}
}
selectionIndex = cText.length;
return TextEditingValue(
text: cText,
selection: TextSelection.collapsed(offset: selectionIndex),
);
}
} // END OF class _DateFormatter
Solution 3:[3]
I found one solution for this, But not an optimized solution, but it is covering almost all the scenarios,
- forward slash during adding fields
- remove the forward slash on clearing fields
- in between editing handling... etc
class CustomDateTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
var text = _format(newValue.text, '/', oldValue);
return newValue.copyWith(
text: text, selection: _updateCursorPosition(text, oldValue));
}
}
String _format(String value, String seperator, TextEditingValue old) {
var finalString = '';
var dd = '';
var mm = '';
var yyy = '';
var oldVal = old.text;
print('<------------------------- start---------------------------->');
print('oldVal -> $oldVal');
print('value -> $value');
var temp_oldVal = oldVal;
var temp_value = value;
if (!oldVal.contains(seperator) ||
oldVal.isEmpty ||
seperator.allMatches(oldVal).length < 2) {
oldVal += '///';
}
if (!value.contains(seperator) || _backSlashCount(value) < 2) {
value += '///';
}
var splitArrOLD = oldVal.split(seperator);
var splitArrNEW = value.split(seperator);
print('----> splitArrOLD: $splitArrOLD');
print('----> splitArrNEW: $splitArrNEW');
for (var i = 0; i < 3; i++) {
splitArrOLD[i] = splitArrOLD[i].toString().trim();
splitArrNEW[i] = splitArrNEW[i].toString().trim();
}
// block erasing
if ((splitArrOLD[0].isNotEmpty &&
splitArrOLD[2].isNotEmpty &&
splitArrOLD[1].isEmpty &&
temp_value.length < temp_oldVal.length &&
splitArrOLD[0] == splitArrNEW[0] &&
splitArrOLD[2].toString().trim() ==
splitArrNEW[1].toString().trim()) ||
(_backSlashCount(temp_oldVal) > _backSlashCount(temp_value) &&
splitArrNEW[1].length > 2) ||
(splitArrNEW[0].length > 2 && _backSlashCount(temp_oldVal) == 1) ||
(_backSlashCount(temp_oldVal) == 2 &&
_backSlashCount(temp_value) == 1 &&
splitArrNEW[0].length > splitArrOLD[0].length)) {
finalString = temp_oldVal; // making the old date as it is
print('blocked finalString : $finalString ');
} else {
if (splitArrNEW[0].length > splitArrOLD[0].length) {
if (splitArrNEW[0].length < 3) {
dd = splitArrNEW[0];
} else {
for (var i = 0; i < 2; i++) {
dd += splitArrNEW[0][i];
}
}
if (dd.length == 2 && !dd.contains(seperator)) {
dd += seperator;
}
} else if (splitArrNEW[0].length == splitArrOLD[0].length) {
print('splitArrNEW[0].length == 2');
if (oldVal.length > value.length && splitArrNEW[1].isEmpty) {
dd = splitArrNEW[0];
} else {
dd = splitArrNEW[0] + seperator;
}
} else if (splitArrNEW[0].length < splitArrOLD[0].length) {
print('splitArrNEW[0].length < splitArrOLD[0].length');
if (oldVal.length > value.length &&
splitArrNEW[1].isEmpty &&
splitArrNEW[0].isNotEmpty) {
dd = splitArrNEW[0];
} else if (temp_oldVal.length > temp_value.length &&
splitArrNEW[0].isEmpty &&
_backSlashCount(temp_value) == 2) {
dd += seperator;
} else {
if (splitArrNEW[0].isNotEmpty) {
dd = splitArrNEW[0] + seperator;
}
}
}
print('dd value --> $dd');
if (dd.isNotEmpty) {
finalString = dd;
if (dd.length == 2 &&
!dd.contains(seperator) &&
oldVal.length < value.length &&
splitArrNEW[1].isNotEmpty) {
if (seperator.allMatches(dd).isEmpty) {
finalString += seperator;
}
} else if (splitArrNEW[2].isNotEmpty &&
splitArrNEW[1].isEmpty &&
temp_oldVal.length > temp_value.length) {
if (seperator.allMatches(dd).isEmpty) {
finalString += seperator;
}
} else if (oldVal.length < value.length &&
(splitArrNEW[1].isNotEmpty || splitArrNEW[2].isNotEmpty)) {
if (seperator.allMatches(dd).isEmpty) {
finalString += seperator;
}
}
} else if (_backSlashCount(temp_oldVal) == 2 && splitArrNEW[1].isNotEmpty) {
dd += seperator;
}
print('finalString after dd=> $finalString');
if (splitArrNEW[0].length == 3 && splitArrOLD[1].isEmpty) {
mm = splitArrNEW[0][2];
}
if (splitArrNEW[1].length > splitArrOLD[1].length) {
print('splitArrNEW[1].length > splitArrOLD[1].length');
if (splitArrNEW[1].length < 3) {
mm = splitArrNEW[1];
} else {
for (var i = 0; i < 2; i++) {
mm += splitArrNEW[1][i];
}
}
if (mm.length == 2 && !mm.contains(seperator)) {
mm += seperator;
}
} else if (splitArrNEW[1].length == splitArrOLD[1].length) {
print('splitArrNEW[1].length = splitArrOLD[1].length');
if (splitArrNEW[1].isNotEmpty) {
mm = splitArrNEW[1];
}
} else if (splitArrNEW[1].length < splitArrOLD[1].length) {
print('splitArrNEW[1].length < splitArrOLD[1].length');
if (splitArrNEW[1].isNotEmpty) {
mm = splitArrNEW[1] + seperator;
}
}
print('mm value --> $mm');
if (mm.isNotEmpty) {
finalString += mm;
if (mm.length == 2 && !mm.contains(seperator)) {
if (temp_oldVal.length < temp_value.length) {
finalString += seperator;
}
}
}
print('finalString after mm=> $finalString');
if (splitArrNEW[1].length == 3 && splitArrOLD[2].isEmpty) {
yyy = splitArrNEW[1][2];
}
if (splitArrNEW[2].length > splitArrOLD[2].length) {
print('splitArrNEW[2].length > splitArrOLD[2].length');
if (splitArrNEW[2].length < 5) {
yyy = splitArrNEW[2];
} else {
for (var i = 0; i < 4; i++) {
yyy += splitArrNEW[2][i];
}
}
} else if (splitArrNEW[2].length == splitArrOLD[2].length) {
print('splitArrNEW[2].length == splitArrOLD[2].length');
if (splitArrNEW[2].isNotEmpty) {
yyy = splitArrNEW[2];
}
} else if (splitArrNEW[2].length < splitArrOLD[2].length) {
print('splitArrNEW[2].length < splitArrOLD[2].length');
yyy = splitArrNEW[2];
}
print('yyy value --> $yyy');
if (yyy.isNotEmpty) {
if (_backSlashCount(finalString) < 2) {
if (splitArrNEW[0].isEmpty && splitArrNEW[1].isEmpty) {
finalString = seperator + seperator + yyy;
} else {
finalString = finalString + seperator + yyy;
}
} else {
finalString += yyy;
}
} else {
if (_backSlashCount(finalString) > 1 && oldVal.length > value.length) {
var valueUpdate = finalString.split(seperator);
finalString = valueUpdate[0] + seperator + valueUpdate[1];
}
}
print('finalString after yyyy=> $finalString');
}
print('<------------------------- finish---------------------------->');
return finalString;
}
TextSelection _updateCursorPosition(String text, TextEditingValue oldValue) {
var endOffset = max(
oldValue.text.length - oldValue.selection.end,
0,
);
var selectionEnd = text.length - endOffset;
print('My log ---> $selectionEnd');
return TextSelection.fromPosition(TextPosition(offset: selectionEnd));
}
int _backSlashCount(String value) {
return '/'.allMatches(value).length;
}
We can Use our custom formator as in inputFormatters like below
TextField(
// maxLength: 10,
keyboardType: TextInputType.datetime,
controller: _controllerDOB,
focusNode: _focusNodeDOB,
decoration: InputDecoration(
hintText: 'DD/MM/YYYY',
counterText: '',
),
inputFormatters: [
WhitelistingTextInputFormatter(RegExp("[0-9/]")),
LengthLimitingTextInputFormatter(10),
CustomDateTextFormatter(),
],
),
try out this solution.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Nehal |
| Solution 2 | |
| Solution 3 |
