'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;

  1. Validating the date for non-31-day months and leap years. The mods were quite straightforward.

  2. 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,

  1. forward slash during adding fields
  2. remove the forward slash on clearing fields
  3. 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