Flutter TextFormField display value with NumberFormat

Viewed 1353

I'd like to be able to display the value entered in TextFormField with NumberFormat('###,##0.00', 'en_US').

There are no issues in the conversion, but I'm having issues displaying the value of formattedPrice in the TextFormField. I'd also like to set the default value of TextFormField as '0.00'. In my sample code below, the TextFormField restricts input to only have a single decimal symbol/separator and two decimal values.

I'm fine with .00 decimals not displayed when there are no decimals present in the TextFormField value during input, but I'd like the two decimal places format be placed when the value is entered.

TextFormField value displayed when typing TextFormField value displayed when entered
123 123.00
123.5 123.50
123.54 123.54
12,345 12,345.00
12,345.6 12,345.60

Minimal Repro

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _subscriptionFormKey = GlobalKey<FormState>();
  final _subscriptionPriceController = TextEditingController();
  NumberFormat numFormat = NumberFormat('###,###.00', 'en_US');
  NumberFormat numSanitizedFormat = NumberFormat('en_US');
  var currency = 'USD';

  void _validate() {
    if (_subscriptionFormKey.currentState!.validate()) {}
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        key: _subscriptionFormKey,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: TextFormField(
            autofocus: true,
            inputFormatters: [
              FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
            ],
            keyboardType: TextInputType.numberWithOptions(decimal: true),
            validator: (cost) {
              if (cost == null || cost.isEmpty) {
                return 'Empty';
              }
              return null;
            },
            textInputAction: TextInputAction.next,

            /// TODO TextFormField default value
            /// I'd like to be to set '0.00' value by default
            controller: _subscriptionPriceController,
            decoration: InputDecoration(
              hintText: 'Price',
              prefixText:
                  NumberFormat.simpleCurrency(name: currency).currencySymbol,
            ),
            onChanged: (price) {
              /// TODO Display [formattedPrice] in TextFormField
              var formattedPrice = numFormat.format(double.parse(price));
              debugPrint('Formatted $formattedPrice');
              var numSanitized = numSanitizedFormat.parse(price);
              debugPrint('Sanitized: $numSanitized');
              _subscriptionPriceController.value = TextEditingValue(
                text: price,
                selection: TextSelection.collapsed(offset: price.length),
              );
            },
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _validate,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Update:

Thanks to @Andrej for suggesting to use onFieldSubmitted() instead of onChange(). I've updated my code with the fix included. I hope this will help other developers blocked with similar issue.

Code with the fix:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _subscriptionFormKey = GlobalKey<FormState>();
  final _subscriptionPriceController = TextEditingController();
  final NumberFormat numFormat = NumberFormat('###,##0.00', 'en_US');
  final NumberFormat numSanitizedFormat = NumberFormat('en_US');
  var currency = 'USD';
  var defaultPrice = '0.00';

  void _validate() {
    if (_subscriptionFormKey.currentState!.validate()) {}
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        key: _subscriptionFormKey,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: TextFormField(
            autofocus: true,
            inputFormatters: [
              FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')),
            ],
            keyboardType: TextInputType.numberWithOptions(decimal: true),
            validator: (cost) {
              if (cost == null || cost.isEmpty) {
                return 'Empty';
              }
              return null;
            },
            textInputAction: TextInputAction.next,

            /// Set TextFormField default value
            controller: _subscriptionPriceController..text = defaultPrice,
            decoration: InputDecoration(
              hintText: 'Price',
              prefixText:
                  NumberFormat.simpleCurrency(name: currency).currencySymbol,
            ),
            onTap: () {
              var textFieldNum = _subscriptionPriceController.value.text;
              var numSanitized = numSanitizedFormat.parse(textFieldNum);
              _subscriptionPriceController.value = TextEditingValue(
                /// Clear if TextFormField value is 0
                text: numSanitized == 0 ? '' : '$numSanitized',
                selection:
                    TextSelection.collapsed(offset: '$numSanitized'.length),
              );
            },
            onFieldSubmitted: (price) {
              /// Set value to 0 if TextFormField value is empty
              if (price == '') price = '0';
              final formattedPrice = numFormat.format(double.parse(price));
              debugPrint('Formatted $formattedPrice');
              _subscriptionPriceController.value = TextEditingValue(
                text: formattedPrice,
                selection:
                    TextSelection.collapsed(offset: formattedPrice.length),
              );
            },
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _validate,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Demo

1 Answers

If I understood you correctly, you want the price to be formatted when the text field is entered.

If yes, just add this code to your TextFormField:

            onFieldSubmitted: (price) {
              final formattedPrice = numFormat.format(double.parse(price));
              debugPrint('Formatted $formattedPrice');
              _subscriptionPriceController.value = TextEditingValue(
                text: formattedPrice,
                selection:
                    TextSelection.collapsed(offset: formattedPrice.length),
              );
            },

To add the initial value to the text field:

final _subscriptionPriceController = TextEditingController(text: '0.00');
Related