I'm having some trouble with validating DateFields. In my application I have a table with DateField properties that the users should be able to edit by pressing an edit button. I also have an OK-button that commits the fields and a cancel-button that discards them.
Here's what I want to achieve (of course there are some neat rules that has to be followed):
The current behaviour that I managed to implement does the following:
Now, I tried implementing a wrapper so that I could listen for value changes, but DateField don't have convenient methods like TextField has (eg. setTextChangeEventMode and setTextChangeTimeout). I override valueChange to take care of some of the issues, but it only seems gets called when you change to a valid date, not when you change to an invalid date (you also have to press enter every time when not using the date picker)... instead another validate() gets called in the background, resetting setValidationVisible() all the time.
I even tried creating a CustomDateRangeValidator, but found out it wasn't to much help.
Please help me get this straight, I have tried so many things now and I'm running out of options.
Here's my createField method:
createField(){
// some more code up here...
if (propertyId.equals("Valid From")) {
dField.setImmediate(true);
dField.setRangeStart(new Date());
dField.setRangeEnd(dateUtil.getDate(9999, 12, 31));
dField.setDateOutOfRangeMessage("Date out of range!");
@SuppressWarnings({ "unchecked", "rawtypes" })
TableDataValidatingWrapper<TextField> wField = new TableDataValidatingWrapper(dField);
return wField;
}
// some more code down here...
}
... and here's my wrapper:
public class TableDataValidatingWrapper<T> extends CustomField<T> {
private static final long serialVersionUID = 1L;
protected Field<T> delegate;
public TableDataValidatingWrapper(final Field<T> delegate) {
this.delegate = delegate;
if (delegate instanceof DateField) {
final DateField dateField = (DateField) delegate;
dateField.setCaption("");
dateField.setImmediate(true);
dateField.setInvalidAllowed(false);
dateField.setInvalidCommitted(true);
dateField.setValidationVisible(false);
dateField.addValueChangeListener(new ValueChangeListener() {
private static final long serialVersionUID = 1L;
@Override
public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
try {
dateField.validate();
dateField.setValidationVisible(false);
} catch (InvalidValueException ive) {
//handle exception
} catch (Exception e) {
//handle exception
}
}
});
}
}
//some other overridden methods here...
}
A little complicated but I hope it works (in Vaadin 7).
I use some Apache Commons and Joda-Time helper methods.
Maybe some customization are needed.
public class MyDateField extends CustomField<Date> {
private static final long serialVersionUID = 1L;
private static final DateTimeFormatter DTF;
static {
DTF = DateTimeFormat.forPattern("yyyy-MM-dd"); // set timezone if needed
}
private TextField tf = new TextField();
private DateField df = new DateField();
private Date original;
private Date minDay = new Date();
private Date maxDay = new DateTime(9999, 12, 31, 23, 59).toDate();
private boolean isInnerChange;
private Date convertedDate;
@Override
protected Component initContent() {
tf.setConverter(InnerConverter.INSTANCE);
tf.setTextChangeEventMode(TextChangeEventMode.EAGER); // or LAZY
tf.addTextChangeListener(new TextChangeListener() {
private static final long serialVersionUID = 1L;
@Override
public void textChange(TextChangeEvent event) {
int pos = tf.getCursorPosition();
if (isValid(event.getText())) {
df.setComponentError(null);
isInnerChange = true;
df.setValue(convertedDate);
} else {
df.setComponentError(InnerErrorMessage.INSTANCE);
}
tf.setCursorPosition(pos);
}
});
df.setStyleName("truncated-date-field");
df.addValueChangeListener(new Property.ValueChangeListener() {
private static final long serialVersionUID = 1L;
@Override
public void valueChange(Property.ValueChangeEvent event) {
if (!isInnerChange) {
Date d = df.getValue();
df.setComponentError(isValid(d) ? null : InnerErrorMessage.INSTANCE);
tf.setValue(d == null ? "" : DTF.print(d.getTime()));
}
isInnerChange = false;
}
});
return new HorizontalLayout(tf, df);
}
@Override
public void setPropertyDataSource( @SuppressWarnings("rawtypes") Property newDS) {
tf.setPropertyDataSource(newDS);
if (newDS != null && getType().isAssignableFrom(newDS.getType())) {
original = (Date) newDS.getValue();
} else {
original = null;
}
df.setValue(original);
}
@Override
public void commit() throws SourceException, InvalidValueException {
ErrorMessage em = df.getComponentError();
if (em != null) {
throw new InvalidValueException(em.getFormattedHtmlMessage());
}
tf.commit();
}
@Override
public Class<? extends Date> getType() {
return Date.class;
}
private boolean isValid(String s) {
s = StringUtils.trimToNull(s);
if (s == null) {
convertedDate = null;
return true;
}
try {
return isValid(DTF.parseDateTime(s).toDate());
} catch (Exception e) {
return false;
}
}
private boolean isValid(Date d) {
if (d == null || DateUtils.truncatedEquals(original, d, Calendar.DAY_OF_MONTH)) {
convertedDate = d;
return true;
}
if (DateUtils.truncatedCompareTo(minDay, d, Calendar.DAY_OF_MONTH) <= 0
&& DateUtils.truncatedCompareTo(maxDay, d, Calendar.DAY_OF_MONTH) >= 0) {
convertedDate = d;
return true;
}
return false;
}
// other methods if needed
private static class InnerErrorMessage implements ErrorMessage {
private static final long serialVersionUID = 1L;
private static final InnerErrorMessage INSTANCE = new InnerErrorMessage();
@Override
public String getFormattedHtmlMessage() {
return "Invalid date!";
}
@Override
public ErrorLevel getErrorLevel() {
return ErrorLevel.ERROR;
}
private Object readResolve() {
return INSTANCE; // preserves singleton property
}
}
private static class InnerConverter implements Converter<String, Date> {
private static final long serialVersionUID = 1L;
private static final InnerConverter INSTANCE = new InnerConverter();
@Override
public Date convertToModel(String value, Class<? extends Date> targetType, Locale locale)
throws ConversionException {
String s = StringUtils.trimToNull(value);
if (s == null) {
return null;
}
try {
return DTF.parseDateTime(s).toDate();
} catch (Exception e) {
throw new ConversionException(e);
}
}
@Override
public String convertToPresentation(Date value, Class<? extends String> targetType, Locale locale)
throws ConversionException {
return value == null ? "" : DTF.print(value.getTime());
}
@Override
public Class<Date> getModelType() {
return Date.class;
}
@Override
public Class<String> getPresentationType() {
return String.class;
}
private Object readResolve() {
return INSTANCE; // preserves singleton property
}
}
}
Into your styles.css:
.truncated-date-field > input.v-datefield-textfield {
display: none;
}