Vaadin 8 Converter - Doesn't update view

I have a converter that I have used in a Vaadin 7 application that turns a BigDecimal into a String for presentation and the String back into a BigDecimal for the model.
It will add an € symbol and some formating, For example it will convert “69” into “€ 69,00”.
When a user types “69” and leave the field the converter will do its thing and then the field will be filled with “€ 69,00”.

In Vaadin 8 some things are changed. Converters are now added to the binder.
So when I bind a textfield and it gets the inital value of “69” the converter will correctly present it as “€ 69,00”,
but when the users changes the field to “70” the model will be updated correctly but the presentation model stays what the user has typed “70”.

How can I get the old behavior back, because when a converter changes the user input then the user should see that, so it can correct it when the converter converts it to something wrong. I think this should be the default behavior?

Should it be the default behavior or am I doing something wrong?

EuroConverter:

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import com.vaadin.data.Result;
import com.vaadin.data.ValueContext;
import com.vaadin.data.converter.StringToBigDecimalConverter;

/**
 * A converter that adds/removes the euro sign and formats currencies with two
 * decimal places.
 */
public class EuroConverter extends StringToBigDecimalConverter{

  public EuroConverter(){
    super("defaultErrorMessage");
  }

  public EuroConverter(String errorMessage){
    super(errorMessage);
  }

  @Override
  public Result<BigDecimal> convertToModel(String value, ValueContext context){
    if(StringUtils.isBlank(value)){
      return Result.ok(null);
    }
    value = value.replaceAll("[€\\s]
", "").trim();
    if(StringUtils.isBlank(value)){
      value = "0";
    }
    return super.convertToModel(value, context);
  }

  @Override
  public String convertToPresentation(BigDecimal value, ValueContext context){
    if(value == null){
      return convertToPresentation(BigDecimal.ZERO, context);
    }
    return "€ " + super.convertToPresentation(value, context);
  }

  @Override
  protected NumberFormat getFormat(Locale locale){
    // Always display currency with two decimals
    NumberFormat format = super.getFormat(locale);
    if(format instanceof DecimalFormat){
      ((DecimalFormat)format).setMaximumFractionDigits(2);
      ((DecimalFormat)format).setMinimumFractionDigits(2);
    }
    return format;
  }
}

Vaadin UI test code:

TextField inputField = new TextField("Test");
Binder<PiggyBank> binder = new Binder<PiggyBank>();

// PiggyBank only has a property `amount` with getter & setter
PiggyBank piggyBank = new PiggyBank ();
piggyBank.setAmount(1234);

binder.forField(inputField)
      .withConverter(new EuroConverter())
      .bind(PiggyBank ::getAmount, PiggyBank ::setAmount);
//1
binder.setBean(piggyBank);
//2

addComponent(inputField);

I’m interested in this

Is there any solution?

Possible solution:

  1. Extend Textfield to modify visible text on each onBlur event:
@SuppressWarnings("serial")
class AutoFormattingTextField extends TextField {

	private SerializableSupplier<String> formatter;

	public AutoFormattingTextField() {
		addBlurListener(e -> autoFormat());
	}

	public void setFormatter(SerializableSupplier<String> formatter) {
		this.formatter = formatter;
	}

	private void autoFormat() {
		getState(true).text = formatter.get();
	}

}
  1. Store both converter and binder and use them to format value:
public class FormatOnBlurDemo {

	private com.vaadin.data.Converter<String, Decimal> converter;
	private ErrorReportingBinder binder;

	public TextField createTextField(String[] args) {
		final AutoFormattingTextField textField = new AutoFormattingTextField();
		textField.setFormatter(() -> {
			// do not rewrite value if it is wrong, only if it is ok
			if (binder != null && binder.getBean() != null && !binder.hasValidationError() && converter != null) {
				Decimal value = binder.getBean().getValue();
				return converter.convertToPresentation(value, new ValueContext(textField));
			}

			return textField.getValue();
		});
		return textField;
	}
}

// dummy class for demo
class MyBean {

	Decimal value;

	public Decimal getValue() {
		return value;
	}

	public void setValue(Decimal value) {
		this.value = value;
	}
}
  1. Finally, you need binder that remembers how last attempt at validation/conversion ended, so that you dont rewrite visible value if it is wrong (so that user has a chance to go back to and fix it):
@SuppressWarnings("serial")
class ErrorReportingBinder extends Binder<MyBean> {

	private boolean lastValidationHadError = false;

	@Override
	public void handleValidationStatus(BindingValidationStatus<?> status) {
		super.handleValidationStatus(status);

		lastValidationHadError = status.isError();
	}

	public boolean hasValidationError() {
		return lastValidationHadError;
	}

}

Thanks to Maria Jurcovicova,

I have a solution, which works for me:

public class CurrencyTextField extends TextField {

	private SerializableSupplier<String> formatter;

	public CurrencyTextField() {
		addBlurListener(e -> autoFormat());
	}

	public void setFormatter(SerializableSupplier<String> formatter) {
		this.formatter = formatter;
	}

	private void autoFormat() {
		try {
			String text = formatter.get();
			
			// formatting Text
			DecimalFormat df = new DecimalFormat("###,##0.00");
			Double d = Double.valueOf(text);
			String formatted = df.format(d);

			getState(true).text = formatted;
		} catch (Exception e) {

		}
	}
}
CurrencyTextField field = new CurrencyTextField();

field.setFormatter(() -> {
	return field.getValue();
});

Tatu created a PR which should hopefully resolve this issue: https://github.com/vaadin/framework/pull/12132

Fix to this issue will be included in Vaadin 8.12.1 version.

How can I format my text value when my user is typing? I need to format a currency textfield, and other types like date, and CPF (Brazil document) 222.222.222-22

My code:

Class VendaView

public class VendaView extends Div {

private static final long serialVersionUID = 1L;

public VendaView() throws ValidationException {

	HorizontalLayout layoutInterno1 = new HorizontalLayout();

	List<Venda> listaVendas = new ArrayList<>();
	Grid<Venda> gridVendas = new Grid<>();
	gridVendas.setItems(listaVendas);
	gridVendas.getHeaderRows();

	gridVendas.addColumn(Venda::getCliente).setHeader("Nome Completo").setAutoWidth(true);
	gridVendas.addColumn(Venda::getCpf).setHeader("CPF").setAutoWidth(true);
	gridVendas.addColumn(Venda::getTelefone).setHeader("Fone").setAutoWidth(true);
	gridVendas.addColumn(Venda::getProdutoComprado).setHeader("Produto").setAutoWidth(true);
	gridVendas.addColumn(Venda::getQuantidade).setHeader("Quantidade").setAutoWidth(true);
	gridVendas.addColumn(Venda::getValorUnitario).setHeader("Valor Unitário").setAutoWidth(true);
	gridVendas.addColumn(Venda::getValorTotal).setHeader("Valor Total").setAutoWidth(true);
	gridVendas.addColumn(Venda::getEndereco).setHeader("Endereço de Entrega").setAutoWidth(true);
	gridVendas.addColumn(Venda::getNumeroResidencial).setHeader("Nº").setAutoWidth(true);
	gridVendas.addColumn(Venda::getBairro).setHeader("Bairro").setAutoWidth(true);
	gridVendas.addColumn(Venda::getCidade).setHeader("Cidade").setAutoWidth(true);
	gridVendas.addColumn(Venda::getEstado).setHeader("Estado").setAutoWidth(true);
	gridVendas.addColumn(Venda::getDataEntrega).setHeader("Data Entrega").setAutoWidth(true);
	gridVendas.addColumn(Venda::getHoraEntrega).setHeader("Hora Entrega").setAutoWidth(true);

	gridVendas.addThemeVariants(GridVariant.LUMO_COMPACT, GridVariant.LUMO_COLUMN_BORDERS);

	gridVendas.setHeight("835px");
	add(gridVendas);

	layoutInterno1.add(gridVendas);

	HorizontalLayout layoutInterno2 = new HorizontalLayout();

	Dialog janelaVendas = new Dialog();

	janelaVendas.setCloseOnEsc(false);
	janelaVendas.setCloseOnOutsideClick(false);

	janelaVendas.setWidth("1100px");
	janelaVendas.setHeight("550px");

	Button adicionarVendas = new Button(new Icon(VaadinIcon.PLUS_CIRCLE_O));
	adicionarVendas.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
	adicionarVendas.addClickListener(event -> janelaVendas.open());

	Button removerVendas = new Button(new Icon(VaadinIcon.MINUS_CIRCLE_O));
	removerVendas.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
	removerVendas.addClickListener(event -> {
		listaVendas.remove(listaVendas.size() - 1);
		gridVendas.getDataProvider().refreshAll();
	});

	Binder<Venda> binder = new Binder<>(Venda.class);

	HorizontalLayout layoutJanelaInterno1 = new HorizontalLayout();

	Text titulo = new Text("Adicionar Venda");

	HorizontalLayout layoutJanelaInterno2 = new HorizontalLayout();

	TextField nomeCompleto = new TextField("Nome Completo:");
	nomeCompleto.getValue();
	nomeCompleto.setWidth("50%");

	TextField cpf = new TextField("CPF: ");
	cpf.getValue();
	cpf.setWidth("50%");

	TextField fone = new TextField("Fone: ");
	fone.getValue();
	fone.setWidth("50%");

	HorizontalLayout layoutJanelaInterno3 = new HorizontalLayout();

	TextField produto = new TextField("Produto: ");
	produto.getValue();
	produto.setWidth("40%");

	TextField quantidade = new TextField("Quantidade: ");
	quantidade.getValue();
	quantidade.setWidth("40%");

	TextField valorUnitario = new TextField("Valor Unitário: ");
	valorUnitario.getValue();
	valorUnitario.setWidth("50%");

	TextField valorTotal = new TextField("Valor Total: ");
	valorTotal.getValue();
	valorTotal.setWidth("50%");

	HorizontalLayout layoutJanelaInterno4 = new HorizontalLayout();

	TextField endereco = new TextField("Endereço de Entrega: ");
	endereco.getValue();
	endereco.setWidth("25%");

	TextField numero = new TextField("Nº: ");
	numero.getValue();
	numero.setWidth("25%");

	TextField bairro = new TextField("Bairro: ");
	bairro.getValue();
	bairro.setWidth("25%");

	TextField cidade = new TextField("Cidade: ");
	cidade.getValue();
	cidade.setWidth("25%");

	TextField estado = new TextField("Estado: ");
	estado.getValue();
	estado.setWidth("25%");

	HorizontalLayout layoutJanelaInterno5 = new HorizontalLayout();

	TextField dataEntrega = new TextField("Data de Entrega: ");
	dataEntrega.getValue();
	dataEntrega.setWidth("195px");

	TextField horaEntrega = new TextField("Hora Entrega: ");
	horaEntrega.getValue();
	horaEntrega.setWidth("195px");

	layoutJanelaInterno1.add(titulo);
	layoutJanelaInterno1.setMargin(isVisible());
	layoutJanelaInterno2.add(nomeCompleto, cpf, fone);
	layoutJanelaInterno2.setMargin(isVisible());
	layoutJanelaInterno3.add(produto, quantidade, valorUnitario, valorTotal);
	layoutJanelaInterno3.setMargin(isVisible());
	layoutJanelaInterno4.add(endereco, numero, bairro, cidade, estado);
	layoutJanelaInterno4.setMargin(isVisible());
	layoutJanelaInterno5.add(dataEntrega, horaEntrega);
	layoutJanelaInterno5.setMargin(isVisible());

	binder.bind(nomeCompleto, Venda::getCliente, Venda::setCliente);
	binder.bind(cpf, Venda::getCpf, Venda::setCpf);
	binder.bind(fone, Venda::getTelefone, Venda::setTelefone);
	binder.bind(produto, Venda::getProdutoComprado, Venda::setProdutoComprado);

	binder.forField(quantidade).withConverter(new StringToIntegerConverter("Valor Inválido"))
			.bind(Venda::getQuantidade, Venda::setQuantidade);
	binder.forField(valorUnitario).withConverter(new StringToDoubleConverter("Valor Inválido"))
			.bind(Venda::getValorUnitario, Venda::setValorUnitario);
	binder.forField(valorTotal).withConverter(new StringToDoubleConverter("Valor Inválido"))
			.bind(Venda::getValorTotal, Venda::setValorTotal);

	binder.bind(endereco, Venda::getEndereco, Venda::setEndereco);

	binder.forField(numero).withConverter(new StringToIntegerConverter("Valor Inválido"))
			.bind(Venda::getNumeroResidencial, Venda::setNumeroResidencial);

	binder.bind(bairro, Venda::getBairro, Venda::setBairro);
	binder.bind(cidade, Venda::getCidade, Venda::setCidade);
	binder.bind(estado, Venda::getEstado, Venda::setEstado);

	binder.bind(dataEntrega, Venda::getDataEntrega, Venda::setDataEntrega);
	binder.bind(horaEntrega, Venda::getHoraEntrega, Venda::setHoraEntrega);

	HorizontalLayout layoutBotoes = new HorizontalLayout();

	Button salvarVendas = new Button("Salvar");
	salvarVendas.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
	salvarVendas.getStyle().set("margin-top", "25px");
	salvarVendas.addClickListener(event -> {
		Venda vendaNova = new Venda();
		listaVendas.add(vendaNova);
		gridVendas.getDataProvider().refreshAll();
		try {
			binder.writeBean(vendaNova);
		} catch (ValidationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		binder.readBean(new Venda());

	});

	Button fecharVendas = new Button("Fechar");
	fecharVendas.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
	fecharVendas.getStyle().set("margin-top", "25px");
	fecharVendas.addClickListener(event -> {
		binder.readBean(new Venda());
		gridVendas.getDataProvider().refreshAll();
		janelaVendas.close();
	});

	janelaVendas.add(layoutJanelaInterno1);
	janelaVendas.add(layoutJanelaInterno2);
	janelaVendas.add(layoutJanelaInterno3);
	janelaVendas.add(layoutJanelaInterno4);
	janelaVendas.add(layoutJanelaInterno5);
	janelaVendas.add(layoutBotoes);

	layoutBotoes.add(salvarVendas, fecharVendas);

	layoutInterno2.add(adicionarVendas, removerVendas);

	add(layoutInterno1, layoutInterno2);

}

}

//==========================================================================================

class Venda

public class Venda implements Cloneable {

private String cliente;
private String cpf;
private String telefone;
private String produtoComprado;
private int quantidade;
private double valorUnitario;
private double valorTotal;
private String endereco;
private int numeroResidencial;
private String bairro;
private String cidade;
private String estado;
private String dataEntrega;
private String horaEntrega;

public Venda() {

}

public String getCliente() {
	return cliente;
}

public void setCliente(String cliente) {
	this.cliente = cliente;
}

public String getCpf() {
	return cpf;
}

public void setCpf(String cpf) {
	this.cpf = cpf;
}

public String getTelefone() {
	return telefone;
}

public void setTelefone(String telefone) {
	this.telefone = telefone;
}

public String getProdutoComprado() {
	return produtoComprado;
}

public void setProdutoComprado(String produtoComprado) {
	this.produtoComprado = produtoComprado;
}

public int getQuantidade() {
	return quantidade;
}

public void setQuantidade(int quantidade) {
	this.quantidade = quantidade;
}

public double getValorUnitario() {
	return valorUnitario;
}

public void setValorUnitario(double valorUnitario) {
	this.valorUnitario = valorUnitario;
}

public double getValorTotal() {
	return valorTotal;
}

public void setValorTotal(double valorTotal) {
	this.valorTotal = valorTotal;
}

public String getEndereco() {
	return endereco;
}

public void setEndereco(String endereco) {
	this.endereco = endereco;
}

public int getNumeroResidencial() {
	return numeroResidencial;
}

public void setNumeroResidencial(int numeroResidencial) {
	this.numeroResidencial = numeroResidencial;
}

public String getBairro() {
	return bairro;
}

public void setBairro(String bairro) {
	this.bairro = bairro;
}

public String getCidade() {
	return cidade;
}

public void setCidade(String cidade) {
	this.cidade = cidade;
}

public String getEstado() {
	return estado;
}

public void setEstado(String estado) {
	this.estado = estado;
}

public String getDataEntrega() {
	return dataEntrega;
}

public void setDataEntrega(String dataEntrega) {
	this.dataEntrega = dataEntrega;
}

public String getHoraEntrega() {
	return horaEntrega;
}

public void setHoraEntrega(String horaEntrega) {
	this.horaEntrega = horaEntrega;
}

}