Search code examples
javajavafxbindinglocaltime

In Java, how can I join a LocalDate and a time formatted String into one observable property of LocalDateTime?


My attempt is to unify the values of a DatePicker (that handles the date selection, but not the time) and a TextField (that handles the time) into an observable LocalDateTime of both joined.

I've set up observable properties for both in the model, but I am having difficulties joining them.

So far I managed to make a few tries with Bindings.createObjectBinding(), but I don't seem to have much success there.

I would like to know at least if I am on the right path or should I go about this differently?


Solution

  • By using LocalDateTime#of(LocalDate,LocalTime) you can create a LocalDateTime from a LocalDate and a LocalTime. What you need now is a way to get an instance of both a LocalDate and a LocalTime. Fortunately, the DatePicker control gives you its value as a LocalDate so we're done there. Next is finding a way to get a LocalTime from a TextField. This is possible by using a TextFormatter and a StringConverter which knows how to convert a String to a LocalTime and vice versa. There's a built-in StringConverter for this use case: LocalTimeStringConverter.

    Once we have both the DatePicker and the TextFormatter we need to create a binding which creates a LocalDateTime from the two values. Since both DatePicker and TextFormatter have a value property, which holds a LocalDate and, in this case, a LocalTime, respectively, creating the binding is relatively simple with Bindings#createObjectBinding(Callable,Observable...).

    DatePicker dp = new DatePicker();
    // Have to associate the TextFormatter with a TextField
    TextFormatter<LocalTime> tf = new TextFormatter<>(new LocalTimeStringConverter());
    
    ObjectBinding<LocalDateTime> binding = Bindings.createObjectBinding(() -> {
        LocalDate ld = dp.getValue();
        LocalTime lt = tf.getValue();
        return ld == null || lt == null ? null : LocalDateTime.of(ld, lt);
    }, dp.valueProperty(), tf.valueProperty());
    

    Here's a full example:

    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.binding.ObjectBinding;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.DatePicker;
    import javafx.scene.control.Label;
    import javafx.scene.control.TextField;
    import javafx.scene.control.TextFormatter;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import javafx.util.converter.LocalTimeStringConverter;
    
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    import java.time.format.DateTimeFormatter;
    
    public class App extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        DatePicker datePicker = new DatePicker();
        datePicker.setValue(LocalDate.now());
    
        TextField timeField = new TextField();
        TextFormatter<LocalTime> timeFieldFormatter =
            new TextFormatter<>(new LocalTimeStringConverter());
        timeField.setTextFormatter(timeFieldFormatter);
        timeFieldFormatter.setValue(LocalTime.now());
    
        HBox dateTimeBox = new HBox(10, datePicker, timeField);
        dateTimeBox.setAlignment(Pos.CENTER);
    
        ObjectBinding<LocalDateTime> ldtBinding = Bindings.createObjectBinding(() -> {
          LocalDate date = datePicker.getValue();
          LocalTime time = timeFieldFormatter.getValue();
          return date == null || time == null ? null : LocalDateTime.of(date, time);
        }, datePicker.valueProperty(), timeFieldFormatter.valueProperty());
    
        Label ldtLabel = new Label();
        ldtLabel.textProperty().bind(Bindings.createStringBinding(() -> {
          LocalDateTime dateTime = ldtBinding.get();
          return dateTime == null ? null : dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        }, ldtBinding));
    
        VBox root = new VBox(15, dateTimeBox, ldtLabel);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(25));
    
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
      }
    }
    

    The above binds the text of a Label to the ObjectBinding<LocalDateTime>. The value of the TextFormatter will update whenever the text is "committed" (e.g. by pressing Enter while the TextField has focus).

    The way I constructed the LocalTimeStringConverter means it will use my Locale and FormatStyle.SHORT for both parsing and formatting the LocalTime. As an example, for me that means something like 3:30 PM or 11:25 AM. This is customizable—see the various constructors of LocalTimeStringConverter.