Flutter inputformatter for date

Viewed 6827

I am looking for an example of an inputformatter for text field that will be a date mm/dd/yyyy, what I am trying to do as the user types update the format. For instance user starts to type mm and the / is put in, then when the dd is typed in the / is put in.

Anyone done this or have an example? I have done it in other languages but could not find a similar way to do in flutter/dart.

This is what I have tried so far, but can not get the logic correct. Any ideas?

    class _DateFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue,
      TextEditingValue newValue
      ) {
    final int newTextLength = newValue.text.length;
    int selectionIndex = newValue.selection.end;
    int usedSubstringIndex = 0;
    final StringBuffer newText = new StringBuffer();
    if (newTextLength == 2) {
      newText.write(newValue.text.substring(0, 2) + '/ ');
      if (newValue.selection.end == 3)
        selectionIndex+=3;
    }
    if (newTextLength == 5) {
      newText.write(newValue.text.substring(0, 5) + '/ ');
      if (newValue.selection.end == 6)
        selectionIndex += 6;
    }
    // Dump the rest.
    if (newTextLength >= usedSubstringIndex)
      newText.write(newValue.text.substring(usedSubstringIndex));
    return new TextEditingValue(
      text: newText.toString(),
      selection: new TextSelection.collapsed(offset: selectionIndex),
    );
  }
}

Thanks

5 Answers

I was struggling with this too. I ended up with the following not so elegant solution:

class DateInputTextField extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _DateInputTextFieldState();
  }
}

class _DateInputTextFieldState extends State<DateInputTextField> {
  @override
  Widget build(BuildContext context) {
    return TextField(
      keyboardType: TextInputType.number,
      inputFormatters: [DateTextFormatter()],
      onChanged: (String value) {},
    );
  }
}

class DateTextFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {

    //this fixes backspace bug
    if (oldValue.text.length >= newValue.text.length) {
      return newValue;
    }

    var dateText = _addSeperators(newValue.text, '/');
    return newValue.copyWith(text: dateText, selection: updateCursorPosition(dateText));
  }

  String _addSeperators(String value, String seperator) {
    value = value.replaceAll('/', '');
    var newString = '';
    for (int i = 0; i < value.length; i++) {
      newString += value[i];
      if (i == 1) {
        newString += seperator;
      }
      if (i == 3) {
        newString += seperator;
      }
    }
    return newString;
  }

  TextSelection updateCursorPosition(String text) {
    return TextSelection.fromPosition(TextPosition(offset: text.length));
  }
}

Here is an improved version based on Jochem Toolenaar answer. This version will automatically remove the forward slashes for you and limit the user to 8 digits.

class DateTextFormatter extends TextInputFormatter {
  static const _maxChars = 8;

  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    var text = _format(newValue.text, '/');
    return newValue.copyWith(text: text, selection: updateCursorPosition(text));
  }

  String _format(String value, String seperator) {
    value = value.replaceAll(seperator, '');
    var newString = '';

    for (int i = 0; i < min(value.length, _maxChars); i++) {
      newString += value[i];
      if ((i == 1 || i == 3) && i != value.length - 1) {
        newString += seperator;
      }
    }

    return newString;
  }

  TextSelection updateCursorPosition(String text) {
    return TextSelection.fromPosition(TextPosition(offset: text.length));
  }
}

Adding an updated version based on Arizona1911 and Jochem Toolenaar versions.

This one prevents the caret from jumping when modifying some of the text that was already typed.

enter image description here

class DateTextFormatter extends TextInputFormatter {
  static const _maxChars = 8;

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    String separator = '/';
    var text = _format(
      newValue.text,
      oldValue.text,
      separator,
    );

    return newValue.copyWith(
      text: text,
      selection: updateCursorPosition(
        oldValue,
        text,
      ),
    );
  }

  String _format(
    String value,
    String oldValue,
    String separator,
  ) {
    var isErasing = value.length < oldValue.length;
    var isComplete = value.length > _maxChars + 2;

    if (!isErasing && isComplete) {
      return oldValue;
    }

    value = value.replaceAll(separator, '');
    final result = <String>[];

    for (int i = 0; i < min(value.length, _maxChars); i++) {
      result.add(value[i]);
      if ((i == 1 || i == 3) && i != value.length - 1) {
        result.add(separator);
      }
    }

    return result.join();
  }

  TextSelection updateCursorPosition(
    TextEditingValue oldValue,
    String text,
  ) {
    var endOffset = max(
      oldValue.text.length - oldValue.selection.end,
      0,
    );

    var selectionEnd = text.length - endOffset;

    return TextSelection.fromPosition(TextPosition(offset: selectionEnd));
  }
}

Thanks to caseyryan/flutter_multi_formatter

In the above solution, I found one overlapping problem with editing in between dates ( if try to edit the month field, the year values get overlapping)

So, 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 on clearing fields
     3. 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, thank you. !

class DateFormatter extends TextInputFormatter {
    @override
    TextEditingValue formatEditUpdate(
        TextEditingValue oldValue,
        TextEditingValue newValue
        ) {
      final int newTextLength = newValue.text.length;
      int selectionIndex = newValue.selection.end;
      int usedSubstringIndex = 0;
      final StringBuffer newText = StringBuffer();
      if (newTextLength >= 3) {
        newText.write(newValue.text.substring(0, usedSubstringIndex = 2) + '/');
        if (newValue.selection.end >= 2)
          selectionIndex++;
      }
      if (newTextLength >= 5) {
        newText.write(newValue.text.substring(2, usedSubstringIndex = 4) + '/');
        if (newValue.selection.end >= 4)
          selectionIndex++;
      }

      // Dump the rest.
      if (newTextLength >= usedSubstringIndex)
        newText.write(newValue.text.substring(usedSubstringIndex));
      return TextEditingValue(`enter code here`
        text: newText.toString(),
        selection: TextSelection.collapsed(offset: selectionIndex),
      );
    }

Related