Robert
Robert

Reputation: 5865

Flutter inputformatter for date

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

Upvotes: 10

Views: 12990

Answers (8)

Noah
Noah

Reputation: 701

An improvement of the original answer (Jochem Toolenaar and Alberto)

This would also validate the month in mm/yy format. That is months such as 90, won't be valid.

import 'dart:math' as math;
import 'package:flutter/services.dart';

/// Date formatter that will automatically remove the forward slashes for you and limit the user to 8 digits. Also validates the month
class DateTextFormatter extends TextInputFormatter {
  static const _maxChars = 5;

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

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

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

    return newString;
  }

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

  bool isValidDate(String value) {
    if (value.length == 1) {
      return ['0', '1'].contains(value);
    } else if (value.length == 2) {
      var number = int.parse(value);
      return number >= 1 && number <= 12;
    } else {
      return true;
    }
  }
}

Also note that you have to include this line:

inputFormatters: [FilteringTextInputFormatter.digitsOnly, DateTextFormatter()]

To enable the input filter character that are not numbers

Upvotes: 0

Vignesh N U
Vignesh N U

Reputation: 1

I found this as a simple solution

inputFormatters: [
                  LengthLimitingTextInputFormatter(10),
                  FilteringTextInputFormatter.allow(RegExp(r'^[0-9\/]*$')),
                  TextInputFormatter.withFunction((oldValue, newValue) {
                    String text = newValue.text;
                    if (newValue.text.length == 2 &&
                        oldValue.text.length !=3) {
                      text += '/';
                    }
                    if (newValue.text.length == 5 && oldValue.text.length !=6) {
                      text += '/';
                    }
                    return TextEditingValue(text: text);
                  })
                ],

Upvotes: 0

Acodi Systems
Acodi Systems

Reputation: 1

I used Robert TextInputFormatter as follows

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

I call the class in a list of TextInputFormatter

List<TextInputFormatter> getTextInputFormatter(int idInputFormatter) {
...
return list = [
    **DATETextInputFormatter()**,
    FilteringTextInputFormatter.allow(RegExp('[0-9/]')),
    LengthLimitingTextInputFormatter(10)]

Finally in the TextFormField(

TextFormField(
...
autovalidateMode:AutovalidateMode.always,
inputFormatters:getTextInputFormatter(idInputFormatter),
keyboardType: TextInputType.number,
validator: (value) {if (value == null ||                                              
value.isEmpty) { return 'need to inform date';}
if (value.length >= 10) {try {if (int.parse(value.substring(0, 
2))>12) {return 'invalid date';}if (int.parse(value.substring(3, 
5)) >31) {return 'invalid date';}var data =                                                  
DateFormat('MM/dd/yyyy').parse(value);                                                  
//test date is between 100 old or 100 years ahead
return ((data.isAfter(DateTime.now().add(Duration(days:36500)))) ||                                                          
(data.isBefore(DateTime.now().subtract(Duration(days:36500)))))
? 'invalid date': null;} catch (e) {return 'invalid date';                                                
}
}
}
return null;
},

Upvotes: -1

MOHAMED SAFVAN KP
MOHAMED SAFVAN KP

Reputation: 51

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. !

Upvotes: 1

Alberto
Alberto

Reputation: 853

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

Upvotes: 7

Arizona1911
Arizona1911

Reputation: 2201

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));
  }
}

Upvotes: 11

Akash Suresh Dandwate
Akash Suresh Dandwate

Reputation: 165

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),
      );
    }

Upvotes: -2

Jochem Toolenaar
Jochem Toolenaar

Reputation: 999

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));
  }
}

Upvotes: 13

Related Questions