Holon-Platform Examples for Vaadin Grid

Holon-Platform works on Properties. So, it is always best to have all your definitions modeled in an interface like here

import com.holonplatform.core.Validator;
import com.holonplatform.core.datastore.DataTarget;
import com.holonplatform.core.property.BooleanProperty;
import com.holonplatform.core.property.NumericProperty;
import com.holonplatform.core.property.PropertySet;
import com.holonplatform.core.property.PropertyValueConverter;
import com.holonplatform.core.property.StringProperty;

/**
 * Product property model
 */
public interface Product {

	public static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Id");

	public static final StringProperty SKU = StringProperty.create("sku");

	public static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description");

	public static final StringProperty CATEGORY = StringProperty.create("category");

	public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price")
			// not negative value validator
			.withValidator(Validator.notNegative());

	public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn")
			// set a property value converter from Integer model type to Boolean
			.converter(PropertyValueConverter.numericBoolean(Integer.class));

	// Product property set
	public static final PropertySet<?> PROPERTY_SET = PropertySet
			.builderOf(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE, WITHDRAWN).withIdentifier(ID).build();

	// "products" DataTarget
	public static final DataTarget<?> TARGET = DataTarget.named("products");

}
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-1', 'Product 1', 'C1', 19.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-2', 'Product 2', 'C2', 29.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-3', 'Product 3', 'C1', 15.00, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-4', 'Product 4', 'C2', 75.50, 1);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-5', 'Product 5', 'C3', 19.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-6', 'Product 6', 'C1', 39.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-7', 'Product 7', 'C4', 44.20, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-8', 'Product 8', 'C1', 77.00, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-9', 'Product 9', 'C3', 23.70, 1);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-10', 'Product 10', 'C2', 92.20, 0);

Note that, every table should have a primary key and if there is none, you cannot update the row using Holon-Platform and if you try to do it, this is the message you would see

2020-07-16 10:16:36.842 WARN 32220 — [nio-8080-exec-4] com.holonplatform.datastore.jdbc : (Save operation) Cannot obtain the primary key for operation [PropertyBoxOperationConfiguration [value=PropertyBox – PROPERTIES: [“transactionId”:java.lang.Long],[“interfaceName”:java.lang.String],[“createTime”:java.time.LocalDateTime] – VALUES: (“transactionId”=1065093643),(“interfaceName”=test),(“createTime”=2020-01-17T08:21:55.983), target=DataTarget [name=Product, type=java.lang.String]]]: an INSERT operation will be performed by default

As it says, it will be doing INSERT operation instead of UPDATE. So, ensure there is a primary key and if not, tell the PropertySet which column to use as the primary key (you can combine multiple columns to make them as primary key if needed) like

public static final PropertySet<?> PROPERTY_SET = PropertySet.builderOf(TRANSACTIONID,INTERFACENAME,CREATETIME)
            .withIdentifier(TRANSACTIONID)
            .build();

Now, we can create any components binding using this model definition. Let’s first see how to create a grid using Holon Platform Listing API

Components.configure(this)
                .fullSize()
                .add(
                        Components.listing.properties(Product.PROPERTY_SET)
                                .dataSource(datastore, Product.TARGET)
                                .build()

                )
        ;

Now, let’s add some properties like

  1. multiSelect
  2. selectAllCheckboxVisibility
  3. columnReorderingAllowed
  4. resizable
  5. editable
  6. customCompnentColumn
public class MainView extends VerticalLayout {

    @Autowired
    private Datastore datastore;

    private PropertyListing listing;

    @PostConstruct
    public void init() {

        Components.configure(this)
                .fullSize()
                .add(listing =
                        Components.listing.properties(Product.PROPERTY_SET)
                                .dataSource(datastore, Product.TARGET)
                                .withThemeVariants(GridVariant.LUMO_COMPACT)
                                .multiSelect()
                                .selectAllCheckboxVisibility(GridMultiSelectionModel.SelectAllCheckboxVisibility.VISIBLE)
                                .columnReorderingAllowed(true)
                                .resizable(true)
                                .editable()
                                .withComponentColumn(properties -> {
                                    IronIcon editIcon = new IronIcon("lumo", "edit");
                                    editIcon.setClassName("size-s");
                                    return Components.button()
                                            .icon(editIcon)
                                            .withThemeName("small")
                                            .onClick(event -> listing.editItem(properties))
                                            .build();
                                })
                                .header("Edit")
                                .add()
                                .build()
                )
        ;
    }
}

So, how can we convert a column values to a component like button or span? Here is an example which shows, how to convert the Withdrawn property into a span component. The steps to do are

  1. Include @JsModule(“@vaadin/vaadin-lumo-styles/badge.js”)
  2. Include @CssImport(value = “./styles/shared-styles.css”, include = “lumo-badge”)
  3. Add a new method componentRenderer to the fluent builder PropertyListing like
.componentRenderer(Product.WITHDRAWN,properties -> {
                                    boolean yes = properties.getValue(Product.WITHDRAWN);
                                    return Components.span()
                                            .text(yes ? "Yes" : "No")
                                            .elementConfiguration(element -> element.setAttribute("theme",yes ? "badge success" : "badge error"))
                                            .build();
                                })

How about the theming? Are there any default themes available to set how the row and column must render? Grid provides some default variants like

.withThemeVariants(GridVariant.LUMO_COMPACT,GridVariant.LUMO_COLUMN_BORDERS,GridVariant.LUMO_ROW_STRIPES)

Editing is also easy. You just have to call the following methods in sequence

editable
editorBuffered(true)
withComponentColumn
editorComponent
withEditorSaveListener

Here is a complete example code

Components.configure(this)
                .fullSize()
                .add(listing =
                        Components.listing.properties(Product.PROPERTY_SET)
                                .dataSource(datastore, Product.TARGET)
                                .withThemeVariants(GridVariant.LUMO_COMPACT, GridVariant.LUMO_COLUMN_BORDERS, GridVariant.LUMO_ROW_STRIPES)
                                .multiSelect()
                                .selectAllCheckboxVisibility(GridMultiSelectionModel.SelectAllCheckboxVisibility.VISIBLE)
                                .columnReorderingAllowed(true)
                                .columnsAutoWidth()
                                .resizable(true)
                                .editable()
                                .editorBuffered(true)
                                .withComponentColumn(properties -> {
                                    IronIcon editIcon = new IronIcon("lumo", "edit");
                                    editIcon.setClassName("size-s");
                                    return Components.button()
                                            .icon(editIcon)
                                            .withThemeName("small")
                                            .onClick(event -> listing.editItem(properties))
                                            .build();
                                })
                                .editorComponent(new Div(
                                        Components.button("Save", event -> {
                                            listing.saveEditingItem();
                                            listing.refreshEditingItem();
                                        }),
                                        Components.button("Cancel", event -> listing.cancelEditing())
                                ))
                                .header("Edit")
                                .add()
                                .withEditorSaveListener(event -> {
                                    PropertyBox propertyBox = event.getItem();

                                    datastore.save(Product.TARGET, propertyBox);
                                    listing.refreshEditingItem();
                                    propertyBox.propertyValues().forEach(System.out::println);
                                    PropertyInputForm inputForm = Components.input.form(Product.PROPERTY_SET)
                                            .build();
                                    inputForm.setValue(propertyBox);
                                    Components.dialog.message()
                                            .sizeUndefined()
                                            .withComponent(inputForm.getComponent())
                                            .open();
                                })
                                .componentRenderer(Product.WITHDRAWN, properties -> {
                                    boolean yes = properties.getValue(Product.WITHDRAWN);
                                    return Components.span()
                                            .text(yes ? "Yes" : "No")
                                            .elementConfiguration(element -> element.setAttribute("theme", yes ? "badge success" : "badge error"))
                                            .build();
                                })

                                .build()
                )
        ;

Now is the time to implement ItemClickListener. I feel it is the better way to show the details or any other action to the user when he/she clicks the row. Here I want to show a dialog window which will have the row values in a form and allow the user to edit the same. Once the YES button is clicked, the editing item is saved to the backend and the row is refreshed to show the new values. The code to achieve this function is

.withItemClickListener(event -> {
                                            showSaveDialog(event.getItem());

The method showSaveDialog() implementation is

private void showSaveDialog(PropertyBox propertyBox) {
        PropertyInputForm inputForm = Components.input.form(Product.PROPERTY_SET)
                .initializer(formLayout -> {
                    formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("490px", 1));
                    formLayout.setWidth("490px");
                })
                .build();
        inputForm.setValue(propertyBox);
        Dialog open = Components.dialog.question(confirmSelected -> {

            if (confirmSelected) {
                save(inputForm.getValue());
            }
        })
                .withComponent(inputForm.getComponent())
                .open();

        open.setCloseOnEsc(true);
    }

    private void save(PropertyBox propertyBox) {

        datastore.save(Product.TARGET, propertyBox);
        listing.refreshItem(propertyBox);
        propertyBox.propertyValues().forEach(objectPropertyValue -> {
            System.out.println(objectPropertyValue.getValue());
        });
    }

When there are thousands of records available in a database table, how do we load them lazily into our grid? The Holon-Platform datastore API makes everything super simple and here is the code to justify that.

.dataSource(DataProvider.fromCallbacks(query -> {

                                    return datastore.query(Product.TARGET).restrict(query.getLimit(),query.getOffset())
                                            .stream(Product.PROPERTY_SET);
                                }, query -> {
                                    return (int) datastore.query(Product.TARGET).count();
                                }))

Do you’ve any other API to make this so simple and that works like a charm? I bet, NO.

Is there any difference in the page loading or user see any difference? I did some local testing and it worked perfectly fine. I’ll have to do some load test to authenticate my above statement.

Next,let us see how to remove a row from the grid. The Datastore API has method to remove an item and the code to use is

datastore.delete(Product.TARGET,properties);

Since it is a virtual column, we can use the same method withComponentColumn and add it to the grid like

.withComponentColumn(properties -> {
                                    return Components.button()
                                            .icon(VaadinIcon.TRASH)
                                            .withThemeName("small")
                                            .withThemeVariants(ButtonVariant.LUMO_ERROR)
                                            .onClick(event -> deleteItem(properties))
                                            .build();
                                })
                                .header("Delete")
                                .add()
private void deleteItem(PropertyBox properties) {
        datastore.delete(Product.TARGET,properties);
        listing.refresh();
    }

Are you wondering what about the validation? What if someone edits the row with invalid values? Does Holon Platform automatically take care of the validations already defined in the model file? Of course, YES. Here is an example to show the validation error when category property goes beyond 10 char limit. Not only this, it also takes care of preventing invalid values entered by the user. For example, if user tries to enter characters in place of numerals, it is automatically prevented by the framework. No, explicit code validation is required.

public static final StringProperty CATEGORY = StringProperty.create("category")
			.withValidator(Validator.max(10))
			;

Next, let’s see how to set some footer and a value to it. If you look into the column UNIT_PRICE, the values are in numeric. So, obviously we want to the total of all Product’s UNIT_PRICE. Here is the code to set footer

.footer(Product.UNIT_PRICE, calculateUnitPrice())
private String calculateUnitPrice() {

        return "Total : " + datastore.query(Product.TARGET)
                .findOne(Product.UNIT_PRICE.sum()).orElse(0.0000);
    }

What if user deletes a product? How can the footer show the updated value? To set the footer accordingly, we need first get the footer cell and then set the value like

listing.getFooter().ifPresent(item -> {
            ItemListing.ItemListingRow<Property<?>> itemListingRow = item.getRows().stream().filter(o -> o.getCell(Product.UNIT_PRICE).isPresent())
                    .findFirst()
                    .orElse(null);

            if (itemListingRow != null) {
                itemListingRow.getCell(Product.UNIT_PRICE).ifPresent(itemListingCell -> itemListingCell.setText(calculateUnitPrice()));
            }
        });
private void deleteItem(PropertyBox properties) {
        datastore.delete(Product.TARGET, properties);
        listing.getFooter().ifPresent(item -> {
            ItemListing.ItemListingRow<Property<?>> itemListingRow = item.getRows().stream().filter(o -> o.getCell(Product.UNIT_PRICE).isPresent())
                    .findFirst()
                    .orElse(null);

            if (itemListingRow != null) {
                itemListingRow.getCell(Product.UNIT_PRICE).ifPresent(itemListingCell -> itemListingCell.setText(calculateUnitPrice()));
            }
        });
        listing.refresh();
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.