The TextFormatter Class in JavaFX: How to Restrict User Input in a Text Field

Posted on Tue 05 April 2016 in JavaFX

There are a lot of code examples for restricting or modifying user input into a JavaFX text field. Most examples I have seen suggest adding a change listener to the text field's text property. Here's how you would allow only lower-case characters in your text field using the change listener approach:

package de.uwesander.javafx.textformatter;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class TextFieldListenerExample extends Application {

    @Override
    public void start(Stage stage) {
        TextField textField = new TextField();
        textField.setMaxWidth(300);

        textField.textProperty().addListener((obs, oldText, newText) -> {
            if (newText.matches("[a-z]")) {
                textField.setText(newText);
            } else {
                textField.setText(oldText);
            }
        });

        StackPane pane = new StackPane(textField);
        Scene scene = new Scene(pane, 400, 200);

        stage.setTitle("TextField Listener Example");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

This approach comes with one drawback: you'll have two events being triggered by two changes of the text property. The first change is caused by the direct user input, the second change is caused by the manipulation of the user input by the change listener. The user won't notice these two events, but somewhere in your code you have another listener for the text property, that listener will receive two events, one with the "invalid" change and a second one with the "valid" change.

Another approach was suggested by Richard Bair a long time ago. His suggestion resulted in the TextFormatter  class being added to JavaFX with version 8u40. It's a clean way to format, filter, or restrict user input. Here's how it works:

package de.uwesander.javafx.textformatter;

import java.util.function.UnaryOperator;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class TextFormatterExample extends Application {

    @Override
    public void start(Stage stage) {
        TextField textField = new TextField();
        textField.setMaxWidth(300);

        TextFormatter<String> textFormatter = getTextFormatter();
        textField.setTextFormatter(textFormatter);

        StackPane pane = new StackPane(textField);
        Scene scene = new Scene(pane, 400, 200);

        stage.setTitle("TextFormatter Example");
        stage.setScene(scene);
        stage.show();
    }

    private TextFormatter<String> getTextFormatter() {
        UnaryOperator<Change> filter = getFilter();
        TextFormatter<String> textFormatter = new TextFormatter<>(filter);
        return textFormatter;
    }

    private UnaryOperator<Change> getFilter() {
        return change -> {
            String text = change.getText();

            if (!change.isContentChange()) {
                return change;
            }

            if (text.matches("[a-z]*") || text.isEmpty()) {
                return change;
            }

            return null;
         };
    }

    public static void main(String[] args) {
        launch(args);
    }
}

What's still missing is support for the backspace key to delete a character in the text field, but that's just my silly example implementation.

[Update 2016-12-28: The implementation was improved to support the deletion and selection of characters.]

This approach intercepts the user input before it's written into the text property and thus fires only one event. Before that happens, you can examine and modify the Change object in the UnaryOperator defined in getFilter() method.

In addition to filtering, a TextFormatter object can convert a value to a string representation and vice versa. From the Javadoc:

A value converter and value  can be used to provide a special format that represents a value of type V. If the control is editable and the text is changed by the user, the value is then updated to correspond to the text.

Sounds pretty handy.