The right place to put massaging of user input?

Say I have a MyTextField that extends TextField, and want to massage user input in some way, like expanding “mo, tu” to “Monday, Tuesday”, and I also want it to play nicely with Binder. Where is the right place to put the logic?

If I do it in a valueChanged listener on the TextField, the Binder will be called twice.

Should I use a converter? Should I override setValue?

I believe both have the disadvantage that I can’t see if the value is from the user, so the code will run also when I just display data from the database.

I see there is a setModelValue I can override, There I could massage the values only if it is from the client, and send them on, but it seems like a bad place to change the display value?

Depends… if you wanna stay on the server side, I would go with a Converter in the Binder (and depending on your requirement… disable or enable pushing this information to the client)

If you can work with the client side… that would make it way easier by extending the client side field and do your logic there

The full solution also involves database lookup, so client-side is out. I also prefer to write as little client-side code as possible.

My first thought was a converter, but I assumed I would have to fight with Vaadin.
I’ve created a small test program to test.

In the program below, I let the bean value start as “mo”, and I had assumed Vaadin would convert that to “Monday” as soon as it was displayed. That did not happen, which surprised me.

This means the converter behaves like I want; It should only convert when user changes the value.

Earlier I’ve had to fight against “writeback”, where if the bean has 3.14159265359, and I had a converter that displays it as, “3.14”, Vaadin would “write back” 3.14 to the bean as well.

Ah, the difference is that in MyWeekdayConverter I adjust the value in convertToModel, while in my 3.14159265359 case I adjust the value in convertToPresentation as well…

So, for my current use-case it looks like I can use a Converter, but Vaadin’s conversion model is still questionable.

package com.ec.traveller;

import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.Result;
import com.vaadin.flow.data.binder.ValueContext;
import com.vaadin.flow.data.converter.Converter;
import com.vaadin.flow.router.Route;

@Route("/testConverter")
public class TestConverter extends VerticalLayout {
    
    static class MyWeekdayConverter implements Converter<String, String> {

        static String[] weekdays = { "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday" };
        
        @Override
        public Result<String> convertToModel(String value, ValueContext context) {
            
            for(String day : weekdays) {
                if(day.startsWith(value)) {
                    return Result.ok(day.substring(0, 1).toUpperCase() + day.substring(1));
                }
            }
            
            return Result.ok(value);
        }

        @Override
        public String convertToPresentation(String value, ValueContext context) {
            return value;
        }

    }

    static class Info {
        String weekday;
        public String getWeekday() { return weekday; }
        public void setWeekday(String weekday) { this.weekday = weekday;}
    }

    public TestConverter() {

        var weekdayField = new TextField("Weekday");
        var binder = new Binder<Info>(Info.class);
        binder.forField(weekdayField)
            .withNullRepresentation("")
            .withConverter(new MyWeekdayConverter())
            .bind(Info::getWeekday, Info::setWeekday); 

        add(weekdayField);
        
        var info = new Info();
        info.setWeekday("mo");
        
        binder.setBean(info);

    }

}

.bind returns a Binding on which you can customize the whole chain as well if you got problems regarding value transformation.

You mentioned database, if you are worried about multiple calls… it is quite easy to build a cache / memorize the latest value / lookup within your concerzer because each field gets his own instance

Yes, for example the write back of the converter can be disabled if you so wish.

But in general I see (at least) two alternative approaches here

  1. Use converter
  2. Wrap the whole logic inside CustomField so that binder sees only one value change

There are pros and cons in both approaches and it depends on application which one is better.

I would look at this from a semantic point of view. It feels like this kind of functionality belongs in the text field rather than in the binding. It’s maybe not relevant in your case but one could easily imagine a case where the substitution should also apply for a standalone field that isn’t used together with Binder.

Since you would want to wrap a regular TextField but customize the server-side value handling, I think AbstractCompositeField might be a good starting point. Your implementation should add a value change listener to the wrapped text field and call its own setModelValue based on the conversion. I guess there’s no need to do a reverse mapping so setPresentationValue could delegate directly to the wrapped text field.

I think AbstractCompositeField is more appropriate than CustomField in this case since the wrapped TextField already has its own label and error indicators. They would be duplicated with CustomField.

My complaint about “writeback” was a digression. I only included it because it was what made me assume a Converter would be useless, and because I find it annoying.

The “writeback” problem comes when it is the output I want to modify. Ie, model has 3.14159265359, but I want to use a converter to display it in a TextField as “3.14”. When I do that, Vaadin immediately converts “3.14” to 3.14 and writes it back to the data model.

See the code below. binder.setBean calls BindingImpl.initFieldValue with writeBackChangedValues = true, with no way to override cleanly.

In my project, the unclean workaround I’ve come up with is to create my own MyBinder where I override setBean in the normal way, but have to use reflection to get references to all the the private methods so that I can call initFieldValue with writeBackChangedValues = false

Vaadin’s Binder :

public void setBean(BEAN bean) {
    if (isRecord) {
        throw new IllegalStateException(
                "setBean can't be used with records, call readBean instead");
    }
    checkBindingsCompleted("setBean");
    if (bean == null) {
        if (this.bean != null) {
            doRemoveBean(true);
            clearFields();
        }
    } else {
        doRemoveBean(false);
        this.bean = bean;
        getBindings().forEach(b -> b.initFieldValue(bean, true));
        // if there has been field value change listeners that trigger
        // validation, need to make sure the validation errors are cleared
        getValidationStatusHandler().statusChange(
                BinderValidationStatus.createUnresolvedStatus(this));
        fireStatusChangeEvent(false);
    }
}

Vaadin’s BindingImpl :

private void initFieldValue(BEAN bean, boolean writeBackChangedValues) {
    assert bean != null;
    valueInit = true;
    try {
        execute(() -> {
            TARGET originalValue = getter.apply(bean);
            convertAndSetFieldValue(originalValue);

            if (writeBackChangedValues && setter != null && !readOnly) {
                doConversion().ifOk(convertedValue -> {
                    if (!Objects.equals(originalValue,
                            convertedValue)) {
                        setter.accept(bean, convertedValue);
                    }
                });
            }
            return null;
        });
    } finally {
        valueInit = false;
    }
}

I do agree that logically the behavior should be part of the Field.
Trying a CustomField was on my block, but I’ll try with an AbstractCompositeField instead