Search code examples
data-bindingvaadinvaadin8vaadin10vaadin-flow

Provide a Converter for data-binding by defining a pair of SerializableFunction objects


In Vaadin 8 Framework, and Vaadin 10 Flow, the data-binding capability lets us provide a Converter to mediate between the widget’s expected data type (such as String for a TextField) and the data type of the backing bean property (such as Integer number).

In this example, the built-in Converter implementation StringToIntegerConverter is used.

binder
.forField( this.phaseField )
.withConverter( 
    new StringToIntegerConverter( "Must enter an integer number" ) 
)
.bind( Panel::getPhase , Panel::setPhase ) ;

But what about defining a Converter for other types? How can I easily define a short-and-sweet Converter? For example, a String-to-UUID converter. I want to show the canonical 36-character hex string in a TextField, and going the other direction, parse that string back into a UUID.

// String to UUID
UUID uuid = UUID.fromString( myString ) ;

// UUID to String
String myString = uuid.toString() ;

I see that Binder.BindingBuilder offers the pair of methods withConverter that both take a pair of SerializableFunction objects.

  • Binder.BindingBuilder::withConverter(SerializableFunction<TARGET,NEWTARGET> toModel, SerializableFunction<NEWTARGET,TARGET> toPresentation)
  • Binder.BindingBuilder::withConverter(SerializableFunction<TARGET,NEWTARGET> toModel, SerializableFunction<NEWTARGET,TARGET> toPresentation, String errorMessage)

➥ So how do I define the pair of SerializableFunction objects/classes?

I noticed that this interface lists a known subinterface ValueProvider<SOURCE,TARGET>. That looks familiar, and I have a hunch it is the key to easily defining a short simple converter. But I do not quite comprehend the syntax with lambdas and all that is going on here.

I am not asking how to write a class implementing Converter. I am asking how to write the pair of SerializableFunction arguments to pass to the Binder.BindingBuilder::withConverter methods listed above as bullet items.

Quoting that JavaDoc:

Interface Binder.BindingBuilder<BEAN,TARGET>

withConverter

default <NEWTARGET> Binder.BindingBuilder<BEAN,NEWTARGET> withConverter(SerializableFunction<TARGET,NEWTARGET> toModel, SerializableFunction<NEWTARGET,TARGET> toPresentation)

Maps the binding to another data type using the mapping functions and a possible exception as the error message.

The mapping functions are used to convert between a presentation type, which must match the current target data type of the binding, and a model type, which can be any data type and becomes the new target type of the binding. When invoking bind(ValueProvider, Setter), the target type of the binding must match the getter/setter types.

For instance, a TextField can be bound to an integer-typed property using appropriate functions such as: withConverter(Integer::valueOf, String::valueOf);

Type Parameters:

NEWTARGET - the type to convert to

Parameters:

toModel - the function which can convert from the old target type to the new target type

toPresentation - the function which can convert from the new target type to the old target type

Returns:

a new binding with the appropriate type

Throws:

IllegalStateException - if bind has already been called


Solution

  • You can do it by passing two lambda expressions to withConverter, so something like this:

    binder.forField(textField)
    .withConverter(text -> UUID.fromString(text), uuid -> uuid.toString())
    .bind(/* ... */);
    

    If you need a more complicated conversion, then the right-hand side of the lambda can be surrounded with brackets, e.g.

    binder.forField(textField).withConverter( text -> {
        if ( text == null ) {
           return something;
        } else {
           return somethingElse;
        } 
    }, uuid -> { return uuid.toString(); } )
    .bind(/* ... */);