Java Arena
Step 3 of 5

Reacting to typing

A button's handler runs when the button is clicked. Often a window should react while the user types: show how many characters are left, say whether a name is free, switch a button on once a field is filled in. For that, a program listens to the text itself.

The text as a property

A text field keeps its text in a property: an object that holds a value and tells others when the value changes. field.textProperty() returns it. Its method addListener takes a ChangeListener, an interface with one method, changed, which has three parameters: the property that changed, the old value and the new value. A lambda with three parameters needs parentheses around them, as module 37's lambdas with two did:

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

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        TextField word = new TextField();
        word.textProperty().addListener((observable, oldValue, newValue) -> {
            System.out.println("\"" + oldValue + "\" to \"" + newValue + "\"");
        });

        stage.setScene(new Scene(new VBox(word)));
        stage.setTitle("Listening");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Type "cat" in text field 1, then replace the text in text field 1 with "dog"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Listening"
  VBox
    TextField "dog"
Output
"" to "c"
"c" to "ca"
"ca" to "cat"
"cat" to "dog"
  • Typing changes the text one character at a time, so typing cat calls the listener three times.
  • The last event replaces the whole text at once, as pasting dog over cat would: that's one change, and one call.
  • The listener hears every change of the text, whoever makes it: the user's typing, and the program's own setText, appendText and clear.
  • It hears only real changes. setText with the text that's already there changes nothing, and the listener isn't called.
  • The first parameter, here called observable, is the property itself. The listeners in this module don't need it, but the lambda must have it, since changed has three parameters.

A sign that counts its characters

A sign maker prints shop signs of up to 20 characters. The window shows how many are left while the customer types:

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

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        TextField text = new TextField();
        text.setPromptText("Text of the sign");
        Label left = new Label("20 characters left");
        text.textProperty().addListener((observable, oldValue, newValue) -> {
            left.setText((20 - newValue.length()) + " characters left");
        });

        stage.setScene(new Scene(new VBox(text, left)));
        stage.setTitle("Sign maker");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Type "FRESH BREAD" in text field 1

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Sign maker"
  VBox
    TextField "FRESH BREAD", prompt "Text of the sign"
    Label "9 characters left"

The label is set from newValue, the text after the change. text.getText() would give the same text inside the listener, since the change has already happened when the listener runs.

Switching a button on and off

setDisable(true) disables a button: it's drawn pale, and clicking it does nothing. setDisable(false) enables it again. The sign maker's Order button should work only when there's a text, and one that fits. The button starts disabled, and the listener switches it on and off as the text changes:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        TextField text = new TextField();
        text.setPromptText("Text of the sign");
        Label left = new Label("20 characters left");
        Button order = new Button("Order");
        order.setDisable(true);
        text.textProperty().addListener((observable, oldValue, newValue) -> {
            left.setText((20 - newValue.length()) + " characters left");
            order.setDisable(newValue.isEmpty() || newValue.length() > 20);
        });
        order.setOnAction(event -> System.out.println("Ordered a sign: " + text.getText()));

        stage.setScene(new Scene(new VBox(text, left, order)));
        stage.setTitle("Sign maker");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Click "Order", type "OPEN LATE ON FRIDAYS!" in text field 1, click "Order", replace the text in text field 1 with "OPEN LATE FRIDAYS", click "Order", then replace the text in text field 1 with ""

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Sign maker"
  VBox
    TextField "", prompt "Text of the sign"
    Label "20 characters left"
    Button "Order" (disabled)
Output
Ordered a sign: OPEN LATE FRIDAYS

The first click comes before any typing, while the button is disabled, so nothing happens. The 21 characters of the first text are one too many, so the second click does nothing either. After the shorter text, the button is enabled, and the third click orders the sign. The last event clears the text, so the button is disabled again: in "The window as text", it's marked (disabled).

setDisable works on any part of a window: a disabled text field can't be typed in, and disabling a pane (a VBox, say) disables everything in it.

Your task

Challenge 1 of 3

A name badge printer shows a preview while the name is typed. Fill in the three blanks, so that the label shows Badge: and the name in capital letters after every change of the text. The task card shows the window after typing Ada Lovelace.

Challenge 2

A shop's checkout has a field for a discount code, with a listener that prints the length of the text before and after each change. appendText adds a whole text with one change, and clear() empties the field. What does the program print?

Challenge 3

A website's invite page asks for a friend's email address. The window is built already: a text field with the prompt text Email address, the label Not an email address yet, and the disabled button Send invite. Make it work:

  • After every change of the text, if it contains @, the label says Ready to send and the button is enabled; if not, the label says Not an email address yet and the button is disabled.
  • Send invite sets the label to Invite sent to and the address, like Invite sent to sam@example.com.

The task card shows the window after typing sam@example.com and clicking Send invite.