One thing that I really like with the current breed of template-based frontend frameworks is how you just define how your components should be configured based on the view’s state, and then the framework takes care of the rest.Based on a binding such as <vaadin-button ?disabled=${!this.hasValidationError}>
, the button is automatically disabled or enabled simply by updating the value of the `hasValidationErrors property.
I set out to investigate how the same concept could be applied for typical Vaadin views built using Java. I ended up building a small framework on top of Vaadin that you can find at https://github.com/Legioth/reactivevaadin.
I would now like to hear what you think about this way of handling the data that is shown in Vaadin UIs. Please leave a comment below or as an issue in the GitHub repository.
A model for viewing an order
Let’s build a component for showing information about an order object. The component is internally using a view model to manage the data that is displayed, even though this isn’t in any way visible to the end user. You can see a live example with some extra tweaking controls running here. All the differences will be in what the code looks like.
A view model contains all the dynamic data that is used to configure the components in a view. You should avoid making changes directly to the UI components, and instead configure them to pull data from the model. But first you need that view model. It’s made up of properties related to the Java bean that is used by the view.
public class OrderModel extends AbstractBeanModel<Order> {
public final Property<String> customerName =
createProperty(Order::getCustomerName);
public final Property<Money> discount =
createProperty(Order::getDiscount);
public OrderModel() {
super(Order::new);
}
}
The model is still incomplete, but we can already create a simple OrderPanel
component that uses an OrderModel
instance to show details about an order bean. It’s still missing all the fun stuff such as a list of ordered products, but it’s a start.
public class OrderPanel extends VerticalLayout {
private OrderModel model = new OrderModel();
public OrderPanel() {
Span customerName = new Span();
Span discount = new Span();
Bindings.bindText(customerName, "Customer: %s", model.customerName);
Bindings.bindText(discount, "Discount: $%s", model.discount);
add(customerName, discount);
}
public void setOrder(Order order) {
model.read(order);
}
}
The setOrder
method only reads values from the bean into the model instance. All the magic happens through the Bindings.bindText
method calls. Whenever the property value changes, its value is formatted using the printf-style format string and then the result is set as the text content of the component.
Note
There could be new API in the components instead of static helpers if this would be built in to the core framework. We could then write something like customerName.setText("Customer: %s", model.customerName)
instead of using the Bindings
class. Kotlin users have the upper hand here thanks to extension functions.
Properties based on other properties
In addition to properties that are populated directly from a bean, you can also define properties by converting or combining other properties. The derived property listens to changes from the original properties so you can use it to create bindings in the same way as with any other property.
Let’s add a property to our model for keeping track of whether there is a discount.
public final Flag hasDiscount =
discount.test(new BigDecimal("0.00")::equals).not();
Note
Cannot use BigDecimal.ZERO
here since we’re using two decimals for all values and equals requires matching precisions.
Flag
is a property that has a boolean value. It gives some additional convenience methods such as not()
which is used here to define a property that negates the value of the flag created by test()
. We can use this property to create a binding that toggles a CSS class name so that some additional styling is used if there is a discount.
Bindings.bindClassName(discount, "discount", model.hasDiscount);
Settable properties for view configuration
All dynamic component configuration is not based on a bean like Order
. The component can also have unrelated configuration properties, such as whether the order discount should be highlighted.
We would thus need a setter in the component that changes the value of a property. For this, we’re using a ModelProperty
which is a simple standalone property with a settable value. We set true as the default value.
private final ModelProperty<Boolean> highlightDiscount = new ModelProperty<>(true);
public void setHighlightDiscount(boolean highlightDiscount) {
this.highlightDiscount.setValue(highlightDiscount);
}
This property can be used to supplement the class name binding so that the class name is only set when both model.hasOrder
and highlightDiscount
are true.
Bindings.bindClassName(discount, "discount",
model.hasDiscount.and(highlightDiscount));
This is the first example that really shows the benefits of deriving all component configuration from model properties. With a traditional way of configuring the components, you must check if the classname should be toggled both when setting and order and when changing the highlight configuration.
Note
It’s not necessary to define the property and the setter in the OrderPanel
component. It could just well be defined by a parent component and then passed down to OrderPanel
as a Property
instance. A binding has a listener registered with its property only while the target component is attached. You don’t need to worry about memory leaks even if you share the same property instance between components with different life cycles.
Repeating multiple components based on a list
Our order model and panel are not yet complete - we’re still missing the list of order lines that each define a product and how many pieces of that product is ordered. We need to create a basic OrderLineModel
class similar to OrderLine
and then we define a property for the lines in OrderModel
. The model property gets a list of OrderLine
beans from the Order
, creates an OrderLineModel
instance for each order and then wraps up those in a java.util.List
instance.
public final ModelList<OrderLineModel> lines =
createList(Order::getOrderLines, OrderLineModel::new);
Note
Vaadin already has a concept for a sequence of items that can fire events when something is changed, namely DataProvider
. It would indeed be possible to create adapters that allow using one in place of the other. It is left as an exercise for the reader to come up with ideas about where that could make sense.
Note
A separate OrderLineModel
class is not really needed for this example since order line values don’t change on the fly. It will be truly useful later on when we create a form around the same model.
To show the order lines as a part of OrderPanel
, we use a repeat binding that creates a component for each item in the model list. The binding automatically adds, removes, or rearranges the components if the list is modified. The component uses bindings to its the line order model to update itself based on changes to any used property value.
Bindings.repeat(orderLines, model.lines, model -> {
Paragraph lineComponent = new Paragraph();
Bindings.bindText(lineComponent, "%s %s x $%s = $%s",
model.product.map(Product::getName),
model.amount,
model.product.map(Product::getPrice),
model.price);
return lineComponent;
});
Note
Binding text with a format string can use multiple properties, just like with the String.format
method. The text is updated whenever any of the properties change.
model.price
is a derived property defined in OrderLineModel
. It takes the price of the product and multiplies it with the amount after converting it into a BigDecimal
.
public final Property<BigDecimal> price =
product.map(Product::getPrice)
.combine(amount.map(BigDecimal::valueOf), BigDecimal::multiply);
Derived properties based on a list
Similarly to ModelProperty
, ModelList
can also be used as a starting point for creating derived lists or single-item properties. You do this using operations familiar from Java’s Stream API. We can define some additional properties in OrderModel
that summarize the status of all the order lines.
public final Flag allInStock =
lines.mapProperties(line -> line.product)
.allMatch(Product::isInStock);
public final Property<BigDecimal> linePriceSum =
lines.mapProperties(line -> line.price)
.reduce(BigDecimal.ZERO, BigDecimal::add);
mapProperties
expands a Property
to its value and also sets up a listener for value changes from that property in addition to listening for changes to the list itself. The rest is really similar to the Stream API except that the result is a Property
or Flag
which will fire events whenever anything used for the computation has changed.
We can finally use these properties to complete the OrderPanel component.
Bindings.bindVisible(allInStock, model.allInStock);
Bindings.bindText(total, "Total $%s - $%s = $%s",
model.linePriceSum,
model.discount,
linePriceSum.combine(discount, BigDecimal::subtract));
An alternative way of deriving properties
If you don’t like the functional style of creating derived properties using operations such as mapProperties
and reduce
, you can also create a derived property based on a single callback that explicitly registers a dependency on any property that it uses.
With that approach, the price property in the order line model can be defined in this way.
public final Property<BigDecimal> price =
Property.computed(using -> {
Product product = using.valueOf(this.product);
int amount = using.valueOf(this.amount);
return product.getPrice().multiply(new BigDecimal(amount));
});
Correspondingly, the sum of all order line prices could be defined like this.
public final Property<BigDecimal> linePriceSum =
Property.computed(using -> {
BigDecimal sum = BigDecimal.ZERO;
for (OrderLineModel line : using.iterableOf(lines)) {
sum = sum.add(using.valueOf(line.price));
}
return sum;
});
Forms based on a view model
The view model has so far been mostly static, except for importing values from another order bean or toggling whether discounts are highlighted. The same principles with a view model, properties and so on can also be used for data that is being edited by the user, i.e. for forms.
A live demo of the fully implemented form runs here.
The first step towards this is to update the view model to not only support reading values from an order bean, but to also be able to write model values back to an order bean instance. To do this, we change the createProperty
calls to also configure a setter for each property At the same time, we can also change the field types to ModelProperty
since the property can now accept new values in other ways than by reading them from a bean.
public final ModelProperty<String> customerName =
createProperty(Order::getCustomerName, Order::setCustomerName);
public final ModelProperty<BigDecimal> discount =
createProperty(Order::getDiscount, Order::setDiscount);
The property for the order lines can be left as before as long as the list returned by Order::getOrderLines
is mutable.
To manage the overall status of the form, e.g. whether the user has made any changes or whether there are any validation errors, we will create a FormBindings
instance. To get a simple initial version of our form, we create some field components and use the form instance to create field bindings between the field component and the corresponding property in the model. We also copy the status labels from the OrderPanel
implementation.
public class OrderForm extends VerticalLayout {
private OrderModel model = new OrderModel();
public OrderForm() {
TextField customerName = new TextField("Customer name");
BigDecimalField discount = new BigDecimalField("Discount");
Span total = new Span();
Span allInStock = new Span("All products are in stock");
FormBindings form = new FormBindings();
form.bind(customerName, model.customerName);
form.bind(discount, model.discount);
Bindings.bindText(total, "Total $%s", model.totalPrice);
Bindings.bindVisible(allInStock, model.allInStock);
add(customerName, discount, allInStock, total);
}
public void setOrder(Order order) {
model.read(order);
}
}
Update a bean based on view model values
Changes to the fields are automatically propagated to the property instances in the view model. Thanks to the bean setters defined for the properties, we can use model.write(order)
to write out the values to an existing order instance, or we can use model.createAndWrite()
to create a new instance and populate it.
We can create a submit button that does either of those depending on whether there is currently an actively edited order or not.
public void setOrder(Order order) {
if (order == null) {
model.reset();
submitButton.setText("Create order");
submitCommand = () -> {
Order newOrder = model.createAndWrite();
orderService.saveNewOrder(newOrder);
};
} else {
model.read(order);
submitButton.setText("Update order");
submitCommand = () -> {
model.write(order);
orderService.updateExistingOrder(order);
};
}
}
Validate and control the submit button
Next, let’s define some simple validation rules for our bindings and hook it up so that the submit button is enabled only when there are changes but no validation errors. Let’s set the customer name as required, add a converter that always stores the discount with two decimals, and require that the discount is positive.
form.bind(customerName, model.customerName)
.setRequired("Must enter customer name");
form.forField(discount)
.withConverter(OrderForm::adjustScale, modelValue -> modelValue)
.withValidator(value -> value.equals(value.abs()),
"Discount cannot be negative")
.bind(model.discount);
The fields will now be marked as invalid but the user can still submit the incomplete form. We should disable the submit button when any binding in form has a validation error. While we’re at it, we can also make the button disabled if there are not yet any changes. FormBindings
provides Flag
for each of those cases, which we can combine for the disabled state of the button.
Bindings.bindEnabled(submitButton,
form.hasChanges.and(form.isValid));
Note
There should probably be a ready-made shorthand binding for this in FormBindings
so that you could just use something like form.canSubmit
. This is once again a case where I choose to show a more explicit variant just to make the relationship between different concepts as clear as possible.
Editing order lines
The last part of the order edit form is to make it possible to add, remove and edit order lines. For this, we need a separate OrderLineForm
component that is created based on an OrderLineModel
instance so that all changes are applied directly to the same view model that is used by the rest of the form. Similarly to the readonly OrderPanel
case, we can use a repeat
binding to automatically add and remove line forms when lines are added or removed in the model. The line editor uses the same FormBindings
instance so that hasChanges
and isValid
also takes the order lines into account.
Bindings.repeat(lines, model.lines, line -> new OrderLineForm(line, form));
The OrderLineForm
component implementation is left as an exercise to the reader since it doesn’t introduce any new concepts.
To add order lines, we add a button that creates a new order line model and adds it to the lines
list. The repeat binding will take care of adding a corresponding line editor component. We preselect a product so that the rest of the logic wouldn’t have to deal with potential null values. We also set the amount to 1.
Button addOrderLine = new Button("+", clickEvent -> {
OrderLineModel newLine = new OrderLineModel();
newLine.product.setValue(orderService.getDefaultProduct());
newLine.amount.setValue(1);
model.lines.add(newLine);
});
Note
Updating the logic to deal with null values in the product
property isn’t very complicated. In most cases, we just need to update derived property mappings such as product.map(Product::getPrice)
to define a default value to use if the property value is null, i.e. product.mapOrDefault(Product::getPrice, BigDecimal.ZERO)
.
The very last step is to make it possible to remove order lines. We add a remove button in the line editor component to run a callback passed to its constructor. The callback only needs to remove the line model from the list of lines models. The repeat binding takes care of removing the line editor component while FormBindings
works so that it doesn’t consider bindings to field components that are not attached.
Bindings.repeat(lines, model.lines,
line -> new OrderLineEditor(line, form,
() -> model.lines.remove(line)));
That’s it. We’re done.
What do you think?
Setting up properties for a view model takes a little bit of extra code, but it helps make the UI logic so much simpler when any change is automatically propagated to all places that do anything based on that value.
Do you think this approach would make sense for whatever UI logic you’re currently building? What parts are difficult to understand? Can the API be improved? Please leave a comment!
If you want to play with the examples or build your own example, then you can get all the code from https://github.com/Legioth/reactivevaadin.