vaadin / flow-components

Java counterpart of Vaadin Web Components
100 stars 66 forks source link

Vaadin 23.3.6 | DatePicker does not provide entered value as intended #4697

Open mab-infokom opened 1 year ago

mab-infokom commented 1 year ago

Description

We are setting up a Vaadin Renderer for our business applications and are struggling with DatePicker currently.

When starting with empty DatePicker Fields, the user can enter a new value on the fly - let's say "17.02.2023".

Now the user has many options that need us to get the entered value.

In all those cases, he did not "finish" his input by typing ENTER, which would cause DatePicker to update its internal value.

Let's take STRG + S as an example:

When our save code runs, it needs current user input in the date field. But there is no way to get it, as DatePicker provides old null value.

We reached out to the Vaadin Chat and got first workaround idea, wich is not suitable for us, but makes this issue reproducable.

image

Expected outcome

We would expect, that we can access valid user input somehow when pressing STRG+S and our save code runs. If user input is not valid, we should be able to recognize this as well in order to abort saving process. As DatePicker has not processed the user input in our use case, we are stuck.

Minimal reproducible example

package com.example.application.views.main.chat;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.router.Route;

@Route("date-picker-instant-value-change")
public class DatePickerInstantValueChange extends HorizontalLayout {

    private DatePicker datePicker;

    private boolean datePickerDirty = false;

    private Button save;

    public DatePickerInstantValueChange() {
        datePicker = new DatePicker();
        datePicker.addValueChangeListener(event -> {
            System.out.println("Value changed even is from client? " + event.isFromClient());
            System.out.println("Value changed from event.getValue(): " + event.getValue());
            System.out.println("Value changed from datePicker.getValue(): " + datePicker.getValue());
            if (datePickerDirty) {
                System.out.println("Because date picker is dirty then we save it");
                System.out.println("Saving datePicker value: " + datePicker.getValue());
                datePickerDirty = false;
            }
        });
        // not needed:
//        datePicker.addClientValidatedEventListener(event -> {
//            System.out.println("Client validated: " + event.toString());
//        });
        datePicker.addOpenedChangeListener(event -> {
            System.out.println("Opened changed: " + event.isOpened());
            // value is still not good at that moment, so we cannot do this:
            if (!event.isFromClient()) {
                System.out.println("Opened triggered programmatically, and value: " + datePicker.getValue());
            }
        });

        save = new Button("Save");
        save.addClickListener(event -> {
            // still have the previous value:
            System.out.println("Saving: " + datePicker.getValue());
        });

        save.addFocusShortcut(Key.KEY_S, KeyModifier.CONTROL);
        UI.getCurrent().addShortcutListener(
                this::shortCutCtrlN, Key.KEY_N,
                KeyModifier.CONTROL);
        // addCtrlSaveShortcutListenerFirst();
        add(datePicker);
        add(save);
    }

    private void shortCutCtrlN() {
        datePicker.setOpened(false);
        datePickerDirty = true;
        System.out.println("Datepicker value, when shortcut CTR+N triggered: " + datePicker.getValue());
        // save click would still save the old value;
        // save.click();
    }

//    private void addCtrlSaveShortcutListenerFirst() {
//        Shortcuts.addShortcutListener(datePicker, () -> {
//            datePicker.setOpened(false);
//            save.focus();
//            save.click();
//            System.out.println("Ctrl+Enter pressed from datePicker" + datePicker.getValue());
//            System.out.println("Saving with CTR+Savefrom datePicker " + datePicker.getValue());
//        }, Key.KEY_S, KeyModifier.CONTROL);
//    }
}

Steps to reproduce

  1. Make given code runnable
  2. Enter a valid date
  3. type STRG+S

Environment

Vaadin Version 23.3.6 OS Linux and Windows

Browsers

Chrome, Firefox, Safari on iOS

TatuLund commented 1 year ago

You should use Binder and enable enforceFieldValidation=true in feature flags in src/main/resources/vaadin-featureflags.properties file.

com.vaadin.experimental.enforceFieldValidation=true See more at: https://github.com/vaadin/platform/issues/3066

That is a new feature we have developed exactly for this kind of use. I.e. to detect input of wrong format in DatePicker or some other fields.

Also it is possible to use DatePicker#addClientValidatedEventListener to detect changes when not using Binder.

TatuLund commented 1 year ago

The question is actually about race condition when using key shortcuts. Here is a more simplified example of the case, which has been solved by using empty JavaScript call to post pone activity to the next roundtrip. This way get correct value.

package org.vaadin.tatu;

import java.time.LocalDate;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyModifier;
import com.vaadin.flow.component.Shortcuts;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.Route;

@Route("form")
public class DatePickerView extends Div {

    public class Person {
        private LocalDate birthDate;
        private String name;

        public LocalDate getBirthDate() {
            return birthDate;
        }

        public void setBirthDate(LocalDate birthDate) {
            this.birthDate = birthDate;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    public DatePickerView() {
        DatePicker datePicker = new DatePicker("Birth date");
        TextField textField = new TextField("Name");
        Person person = new Person();
        Binder<Person> binder = new Binder<>();
        binder.forField(datePicker).asRequired().bind(Person::getBirthDate,
                Person::setBirthDate);
        binder.forField(textField).asRequired().bind(Person::getName,
                Person::setName);
        binder.readBean(person);
        Button save = new Button("Save");
        save.addClickListener(e -> {
            try {
                binder.writeBean(person);
                Notification.show("'" + person.getName() + " "
                        + person.getBirthDate().toString() + "' saved!");
            } catch (ValidationException e1) {
                Notification.show("Not valid");
            }
        });
        Shortcuts.addShortcutListener(save, e -> {
            datePicker.setOpened(false);
            datePicker.getElement().executeJs("return 0;").then(result -> {
                save.focus();
                save.click();
            });
        }, Key.KEY_S, KeyModifier.CONTROL).listenOn(this);
        add(textField, datePicker, save);
    }

}