Not possible to enter a fractional value in a text field with a textChangeTimeout

It is not possible to enter a fractional value in a text field with a textChangeTimeout set like this one:

val field: TextField<BigDecimal> = uiComponents.create(TextField.NAME)
field.textChangeTimeout = 300
field.textChangeEventMode = TextInputField.TextChangeEventMode.LAZY
field.setFormatter { null } // makes no difference...
field.valueSource = ContainerValueSource(table.getInstanceContainer(entity), "amount")

“amount” of entity is a BigDecimal field. You would have to be faster than the 300ms with the comma and the value to be able to enter a fractional value. Is there a way around that problem, but still firing the change event after a timeout?

Hello!

In the formatter you can try to append to the value the fractional part like: “.00” if there is no decimal separator was entered. For instance:

@Autowired
private CurrentAuthentication currentAuthentication;
@Autowired
private DatatypeRegistry datatypeRegistry;
@Autowired
private FormatStringsRegistry formatStringsRegistry;

@Subscribe
public void onInit(InitEvent event) {
   FormatStrings formatStrings = formatStringsRegistry.getFormatStrings(currentAuthentication.getLocale());
   Datatype<BigDecimal> datatype = datatypeRegistry.get(BigDecimal.class);

   textField.setFormatter(bigDecimal -> {
       String formatted = datatype.format(bigDecimal, currentAuthentication.getLocale());
       String decimalSeparator = String.valueOf(formatStrings.getFormatSymbols().getDecimalSeparator());
       if (!formatted.contains(decimalSeparator)) {
           return formatted + decimalSeparator + "00";
       }
       return formatted;
   });
}

Thanks, it’s possible to enter a fractional part then, but the UX is very bad. Do you have another idea maybe?

I’d love to be able to set the input type to behave just the way it behaves with normal string input… Validation should work exactly as it is now, but I don’t want the formatter to do anything. That’s not possible right now, because the field with the value source attached always sets the new value to presentation after change.

The field should change its value on load, but not on user change. It would be ideal, if we can have a propert for that.

There are more complex ways:

  1. Extending TextField component. Override componentValueChanged() method and register this component: Component registration.
  1. Using Vaadin TextField. Subscribe on value change event of TextField and ContainerValueSource. As it’s a Vaadin component, you need to wrap it into the Jmix component, e.g CssLayout:
    CssLayout wrapper = uiComponents.create(CssLayout.class);
    wrapper.unwrap(com.vaadin.ui.CssLayout.class).addComponent(textField);
    
1 Like

Nr 1 I tried already, but the problem is in the setValue() method. This method is also called when the value changes and the origin of the change can’t be differentiated between a “onLoad change” and a user-originated change.

Ok, with a bit of a hack I got it done. I added two properties to the AbstractValueComponent class:

    private boolean updatedValueFirstTime = false;
    public boolean updateValueToPresentation = true;

And changed the setValueToPresentation method as follows:

    protected void setValueToPresentation(@Nullable P value) {
        if (hasValidationError()) {
            setValidationError(null);
        }

        if (updateValueToPresentation || !updatedValueFirstTime) {
            updatedValueFirstTime = true;
            component.setValue(value);
        }
    }

Now I can set the BigDecimal TextField even to the EAGER change mode and got a really nice UX. :slight_smile:

field.textChangeEventMode = TextInputField.TextChangeEventMode.EAGER

@pinyazhin Do you think this is a change I can get upstream?

Could you clarify you extend TextField and there manage updateValueToPresentation option?

If you set value programmatically, the TextField presentation value won’t be updated. But it should work if the value is updated from the client-side, like in your case.

Thanks @pinyazhin and sorry for the late reply. I built my own text field now, the code looks like this:

import io.jmix.ui.component.impl.TextFieldImpl

open class MyTextField<VALUE> : TextFieldImpl<VALUE>() {

    companion object {
        const val NAME = "myTextField"
    }

    private var updatedValueFirstTime = false

    /** If set to false, only the first value retrieved - the loaded value - will be set to presentation.
     * Subsequent changes to the value won't be set to presenatation. */
    var updateValueToPresentation = true

    override fun setValueToPresentation(value: String?) {
        if (hasValidationError()) {
            setValidationError(null)
        }

        if (updateValueToPresentation || !updatedValueFirstTime) {
            updatedValueFirstTime = true
            component.value = value
        }
    }
}

This works. It puts the value the first time it is loaded to presentation and then I can update the value eagerly from the text field without interrupting the input experience of the user.

1 Like