ThousandsSeparatorTextInputFormatter class
The ThousandsSeparatorTextInputFormatter is a TextInputFormatter that formats numeric input with thousands separators while the user types, and preserves the caret or selection as naturally as possible.
This formatter is intended for text fields that accept:
- integer values like
123456->123,456 - decimal values like
123456.32->123,456.32
Features
- Adds thousands separators as you type.
- When digits are inserted or deleted, including in the middle, separators are recalculated (re-positioned).
- You can add a fractional part by typing a dot
.and then more digits. - Only digits and one decimal dot are kept. Commas, minus signs, and other characters are ignored.
- If a dot already exists and you type another one to the right, the new one is ignored.
- If a dot already exists and you type another one to the left, the left one is kept and the old right one is dropped.
- It tries to keep the caret or selection in the natural place after reformatting.
- It has special handling for Backspace just to the right of a thousands separator and Delete just to the left of one, so those keys delete the neighboring digit instead of appearing to do nothing.
- If the input starts with
.,it normalizes it to0.. For example,.5becomes0.5. - Limit the fractional part to allowedDecimals digits.
- Prevent leading zeroes on the left.
Details
The integer part is grouped using groupSeparator, and the fractional part, if present, is left exactly as typed after the first decimalSeparator.
Example behavior:
1->11234->1,23412345.6->12,345.612345.60->12,345.60.5->0.5
This formatter also handles editing in the middle of the text, not only at
the end. For example, if the field contains 123,456.32, the user places
the caret between 1 and 2, and types 5, the result becomes
1,523,456.32, and the caret stays immediately after the inserted 5.
How it works
The formatter treats the visible text and the numeric value as two different representations:
- the formatted text shown in the field, such as
1,234,567.89 - a raw numeric text without grouping separators, such as
1234567.89
On each edit, it performs these steps:
- Sanitize the user's edited text into raw numeric text. Only digits and a single decimal separator are kept.
- Convert the current selection from formatted text offsets into raw text offsets.
- Rebuild the formatted text from the raw text by inserting grouping separators into the integer part.
- Convert the selection back from raw offsets into formatted offsets.
- Return a new TextEditingValue with the formatted text and corrected selection.
The important idea is that the selection is mapped through the raw text, instead of trying to guess its final visible position directly. This makes caret placement much more reliable when separators are inserted or removed during formatting.
Caret and selection mapping
Caret placement is based on text boundaries, not characters. A string of
length N has N + 1 valid caret positions. The formatter builds mapping
tables between:
- formatted text boundaries -> raw text boundaries
- raw text boundaries -> formatted text boundaries
Because of that, both collapsed selections, meaning a caret, and non collapsed selections, meaning highlighted ranges, can be handled using the same logic.
This is especially useful when:
- typing in the middle of the number
- replacing a selected range
- pasting text
- editing values that already contain grouping separators
Special handling for Backspace and Delete near separators
Grouping separators are formatting characters only. They do not exist in the raw numeric value. Because of that, deleting a comma naively would appear to do nothing, since the formatter would simply insert it again when rebuilding the formatted text.
To avoid that, the formatter compares oldValue and newValue and tries to
recover the user's intent when the only removed characters are grouping
separators and the raw numeric value did not actually change.
Two important cases are handled:
- Backspace immediately to the right of a grouping separator
- Delete immediately to the left of a grouping separator
In those cases, the formatter interprets the edit as a request to delete the adjacent digit in raw space, then reformats the result.
Example:
123,456, caret after,, Backspace ->12,456123,456, caret before,, Delete ->12,356
This makes deletion feel much closer to what users expect in a formatted numeric field.
Composition and IME behavior
If the input method is currently composing text, the formatter returns
newValue unchanged. This avoids interfering with active IME composition,
which is important for correct text input behavior on mobile keyboards and
some international input methods.
Normalization rules
The formatter applies a few normalization rules:
- Only digits are kept, plus the first decimal separator.
- Additional decimal separators are ignored.
- If the input starts with the decimal separator, it is normalized to a
leading zero. For example,
.5becomes0.5. - Grouping separators in the input are ignored during parsing and are always rebuilt from the raw value.
Scope and assumptions
This formatter is designed for numeric text entry with:
- ASCII digits
0through9 - a single character grouping separator
- a single character decimal separator
It does not attempt to support:
- negative numbers
- scientific notation
- locale specific digits
- currency symbols
- arbitrary user text
Direct string indexing is used internally, which is safe for this formatter because it intentionally works only with simple ASCII numeric input and single character separators.
Typical usage
TextField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
GroupedDecimalInputFormatter(),
],
)
By default, this formatter uses , as the grouping separator and . as the
decimal separator.
- Inheritance
-
- Object
- TextInputFormatter
- ThousandsSeparatorTextInputFormatter
Constructors
- ThousandsSeparatorTextInputFormatter({String groupSeparator = ',', String decimalSeparator = '.', int allowedDecimals = 1000})
Properties
- allowedDecimals → int
-
final
- decimalSeparator → String
-
final
- groupSeparator → String
-
final
- hashCode → int
-
The hash code for this object.
no setterinherited
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
Methods
-
formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) → TextEditingValue -
Called when text is being typed or cut/copy/pasted in the EditableText.
override
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
toString(
) → String -
A string representation of this object.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited