Skip to content

Commit

Permalink
Implement support for binding single-select components
Browse files Browse the repository at this point in the history
Change-Id: I340e802e5c8e6e036b54f81ec46beeb5e1c34329
  • Loading branch information
jdahlstrom committed Sep 14, 2016
1 parent 41516b5 commit 2a0c590
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 24 deletions.
19 changes: 1 addition & 18 deletions server/src/main/java/com/vaadin/data/BeanBinder.java
Expand Up @@ -20,16 +20,13 @@
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;

import com.vaadin.data.util.BeanUtil;
import com.vaadin.data.util.converter.Converter;
import com.vaadin.data.validator.BeanValidator;
import com.vaadin.ui.Component;
import com.vaadin.ui.UI;

/**
* A {@code Binder} subclass specialized for binding <em>beans</em>: classes
Expand Down Expand Up @@ -177,7 +174,7 @@ public void bind(String propertyName) {

if (BeanValidator.checkBeanValidationAvailable()) {
finalBinding = finalBinding.withValidator(new BeanValidator(
getBinder().beanType, propertyName, getLocale()));
getBinder().beanType, propertyName, findLocale()));
}

PropertyDescriptor descriptor = getDescriptor(propertyName);
Expand Down Expand Up @@ -242,20 +239,6 @@ private Converter<TARGET, Object> createConverter() {
throw new RuntimeException(exception);
});
}

private Locale getLocale() {
Locale l = null;
if (getField() instanceof Component) {
l = ((Component) getField()).getLocale();
}
if (l == null && UI.getCurrent() != null) {
l = UI.getCurrent().getLocale();
}
if (l == null) {
l = Locale.getDefault();
}
return l;
}
}

private final Class<? extends BEAN> beanType;
Expand Down
121 changes: 118 additions & 3 deletions server/src/main/java/com/vaadin/data/Binder.java
Expand Up @@ -38,7 +38,10 @@
import com.vaadin.server.UserError;
import com.vaadin.shared.Registration;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractSingleSelect;
import com.vaadin.ui.Component;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;

/**
* Connects one or more {@code Field} components to properties of a backing data
Expand Down Expand Up @@ -497,6 +500,25 @@ protected void checkUnbound() {
}
}

/**
* Finds an appropriate locale to be used in conversion and validation.
*
* @return the found locale, not null
*/
protected Locale findLocale() {
Locale l = null;
if (getField() instanceof Component) {
l = ((Component) getField()).getLocale();
}
if (l == null && UI.getCurrent() != null) {
l = UI.getCurrent().getLocale();
}
if (l == null) {
l = Locale.getDefault();
}
return l;
}

private void bind(BEAN bean) {
setFieldValue(bean);
onValueChange = getField()
Expand All @@ -521,7 +543,7 @@ public ValidationStatus<TARGET> validate() {
private ValidationStatus<TARGET> doValidation() {
FIELDVALUE fieldValue = field.getValue();
Result<TARGET> dataValue = converterValidatorChain.convertToModel(
fieldValue, ((AbstractComponent) field).getLocale());
fieldValue, findLocale());
return new ValidationStatus<>(this, dataValue);
}

Expand All @@ -543,8 +565,7 @@ private void setFieldValue(BEAN bean) {

private FIELDVALUE convertDataToFieldType(BEAN bean) {
return converterValidatorChain.convertToPresentation(
getter.apply(bean),
((AbstractComponent) getField()).getLocale());
getter.apply(bean), findLocale());
}

/**
Expand Down Expand Up @@ -673,6 +694,8 @@ public Optional<BEAN> getBean() {
* @param field
* the field to be bound, not null
* @return the new binding
*
* @see #bind(HasValue, Function, BiConsumer)
*/
public <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> forField(
HasValue<FIELDVALUE> field) {
Expand All @@ -685,6 +708,45 @@ public <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> forField(
this::handleValidationStatus);
}

/**
* Creates a new binding for the given single select component. The returned
* binding may be further configured before invoking
* {@link Binding#bind(Function, BiConsumer) Binding.bind} which completes
* the binding. Until {@code Binding.bind} is called, the binding has no
* effect.
*
* @param <SELECTVALUE>
* the item type of the select
* @param select
* the select to be bound, not null
* @return the new binding
*
* @see #bind(AbstractSingleSelect, Function, BiConsumer)
*/
public <SELECTVALUE> Binding<BEAN, SELECTVALUE, SELECTVALUE> forSelect(
AbstractSingleSelect<SELECTVALUE> select) {
return forField(new HasValue<SELECTVALUE>() {

@Override
public void setValue(SELECTVALUE value) {
select.setSelectedItem(value);
}

@Override
public SELECTVALUE getValue() {
return select.getSelectedItem().orElse(null);
}

@Override
public Registration addValueChangeListener(
ValueChangeListener<? super SELECTVALUE> listener) {
return select.addSelectionListener(e -> listener.accept(
new ValueChange<>(select, getValue(), e
.isUserOriginated())));
}
});
}

/**
* Binds a field to a bean property represented by the given getter and
* setter pair. The functions are used to update the field value from the
Expand Down Expand Up @@ -734,6 +796,59 @@ public <FIELDVALUE> void bind(HasValue<FIELDVALUE> field,
forField(field).bind(getter, setter);
}

/**
* Binds a single select to a bean property represented by the given getter
* and setter pair. The functions are used to update the selection from the
* property and to store the selection to the property, respectively.
* <p>
* Use the {@link #forSelect(AbstractSingleSelect)} method instead if you
* want to further configure the new binding.
* <p>
* When a bean is bound with {@link Binder#bind(BEAN)}, the selected item is
* set to the return value of the given getter. The property value is then
* updated via the given setter whenever the selected item changes. The
* setter may be null; in that case the property value is never updated and
* the binding is said to be <i>read-only</i>. A null property value
* corresponds to no selection and vice versa.
* <p>
* If the Binder is already bound to some item, the newly bound select is
* associated with the corresponding bean property as described above.
* <p>
* The getter and setter can be arbitrary functions, for instance
* implementing user-defined conversion or validation. However, in the most
* basic use case you can simply pass a pair of method references to this
* method as follows:
*
* <pre>
* class Person {
* public enum Title { MR, MS, MISS, MRS, DR, PROF };
*
* public Title getTitle() { ... }
* public void setTitle(Title title) { ... }
* }
*
* NativeSelect<Title> titleSelect = new NativeSelect<>();
* titleSelect.setItems(Title.values());
* binder.bind(titleSelect, Person::getTitle, Person::setTitle);
* </pre>
*
* @param <SELECTVALUE>
* the item type of the select
* @param select
* the select to bind, not null
* @param getter
* the function to get the value of the property to the
* selection, not null
* @param setter
* the function to save the selection to the property or null if
* read-only
*/
public <SELECTVALUE> void bind(AbstractSingleSelect<SELECTVALUE> select,
Function<BEAN, SELECTVALUE> getter,
BiConsumer<BEAN, SELECTVALUE> setter) {
forSelect(select).bind(getter, setter);
}

/**
* Binds the given bean to all the fields added to this Binder. To remove
* the binding, call {@link #unbind()}.
Expand Down
11 changes: 11 additions & 0 deletions server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java
Expand Up @@ -254,6 +254,17 @@ public Optional<T> getSelectedItem() {
return getSelectionModel().getSelectedItem();
}

/**
* Sets the current selection to the given item or clears selection if given
* {@code null}.
*
* @param item
* the item to select or {@code null} to clear selection
*/
public void setSelectedItem(T item) {
getSelectionModel().setSelectedItem(item);
}

@Override
protected AbstractSingleSelectState getState() {
return (AbstractSingleSelectState) super.getState();
Expand Down
109 changes: 109 additions & 0 deletions server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java
@@ -0,0 +1,109 @@
/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.data;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;

import org.junit.Before;
import org.junit.Test;

import com.vaadin.tests.data.bean.Person;
import com.vaadin.tests.data.bean.Sex;
import com.vaadin.ui.NativeSelect;

public class BinderSingleSelectTest extends
BinderTestBase<Binder<Person>, Person> {

private NativeSelect<Sex> select;

@Before
public void setUp() {
binder = new Binder<>();
item = new Person();
select = new NativeSelect<>();
select.setItems(Sex.values());
}

@Test
public void personBound_bindSelectByShortcut_selectionUpdated() {
item.setSex(Sex.FEMALE);
binder.bind(item);
binder.bind(select, Person::getSex, Person::setSex);

assertSame(Sex.FEMALE, select.getSelectedItem().orElse(null));
}

@Test
public void personBound_bindSelect_selectionUpdated() {
item.setSex(Sex.MALE);
binder.bind(item);
binder.forSelect(select).bind(Person::getSex, Person::setSex);

assertSame(Sex.MALE, select.getSelectedItem().orElse(null));
}

@Test
public void selectBound_bindPersonWithNullSex_selectedItemNotPresent() {
bindSex();

assertFalse(select.getSelectedItem().isPresent());
}

@Test
public void selectBound_bindPerson_selectionUpdated() {
item.setSex(Sex.FEMALE);
bindSex();

assertSame(Sex.FEMALE, select.getSelectedItem().orElse(null));
}

@Test
public void bound_setSelection_beanValueUpdated() {
bindSex();

select.select(Sex.MALE);

assertSame(Sex.MALE, item.getSex());
}

@Test
public void bound_deselect_beanValueUpdatedToNull() {
item.setSex(Sex.MALE);
bindSex();

select.deselect(Sex.MALE);

assertNull(item.getSex());
}

@Test
public void unbound_changeSelection_beanValueNotUpdated() {
item.setSex(Sex.UNKNOWN);
bindSex();
binder.unbind();

select.select(Sex.FEMALE);

assertSame(Sex.UNKNOWN, item.getSex());
}

protected void bindSex() {
binder.forSelect(select).bind(Person::getSex, Person::setSex);
binder.bind(item);
}
}
Expand Up @@ -57,12 +57,29 @@ public interface Single<T> extends SelectionModel<T> {
*/
public Optional<T> getSelectedItem();

/**
* Sets the current selection to the given item, or clears selection if
* given {@code null}.
*
* @param item
* the item to select or {@code null} to clear selection
*/
public default void setSelectedItem(T item) {
if (item != null) {
select(item);
} else {
deselectAll();
}
}

/**
* Returns a singleton set of the currently selected item or an empty
* set if no item is selected.
*
* @return a singleton set of the selected item if any, an empty set
* otherwise
*
* @see #getSelectedItem()
*/
@Override
default Set<T> getSelectedItems() {
Expand All @@ -85,13 +102,13 @@ public interface Multi<T> extends SelectionModel<T> {
*/
@Override
public void select(T item);

}

/**
* Returns an immutable set of the currently selected items.
* Returns an immutable set of the currently selected items. It is safe to
* invoke other {@code SelectionModel} methods while iterating over the set.
* <p>
* <i>Implementation note:</i> the iteration order of the items in the
* <em>Implementation note:</em> the iteration order of the items in the
* returned set should be well-defined and documented by the implementing
* class.
*
Expand Down

0 comments on commit 2a0c590

Please sign in to comment.