CRUD
- Columns
- Editor
- Grid Replacement
- Toolbar
- Sorting & Filtering
- Item Initialization
- Localization
- Related Components
|
Note
|
Commercial Feature
A commercial Vaadin subscription is required to use CRUD in your project. |
CRUD is a component for managing a dataset. It allows for easy displaying, editing, creating, and deleting of items.
Source code
CrudBasic.java
PersonDataProvider.java
PersonDataProvider.java
crud-basic.tsx
crud-basic.ts
Columns
CRUD automatically generates columns for each field in the provided dataset. You can add columns with it and configure or remove existing ones.
Source code
CrudColumns.java
PersonDataProvider.java
PersonDataProvider.java
crud-columns.tsx
crud-columns.ts
Editor
Data is edited using CRUD’s editor UI. By default, the editor is opened by clicking the edit Button in the last column. However, this button column can be removed if you want to provide an alternative way to engage the editor. For example, you set it to open using a double-click like so:
Source code
CrudOpenEditor.java
PersonDataProvider.java
PersonDataProvider.java
crud-open-editor.tsx
crud-open-editor.ts
Editor Position
The editor can be positioned in an overlay, on the side or at the bottom.
Overlay
The overlay position renders the editor in a modal overlay. This is the default position. Overlays aren’t constrained to the CRUD’s size. This makes them ideal for complex forms. However, they do block the user from viewing and interacting with the Grid beneath.
Aside
The aside position displays the editor as an overlay next to the grid. Use this position when there is sufficient horizontal space to accommodate both the grid and the editor. Also, use it when it’s beneficial for the user to be able to view and interact with the grid while the editor is open. Aside positioning is also a good fit for single-column forms.
Source code
CrudEditorAside.java
PersonDataProvider.java
PersonDataProvider.java
crud-editor-aside.tsx
crud-editor-aside.ts
|
Note
|
Grid Width
The opening and closing of an aside editor affect the grid’s width. Fixed-width columns are recommended to prevent them from resizing each time.
|
Bottom
The bottom position can be useful when the user needs to see as many columns in the grid as possible while editing, when horizontal space is limited, or when a wider editor form is desired.
Source code
CrudEditorBottom.java
PersonDataProvider.java
PersonDataProvider.java
crud-editor-bottom.tsx
crud-editor-bottom.ts
When using a bottom-positioned editor, make sure there’s enough vertical space to fit comfortably both the grid and the editor. Incidentally, a bottom-positioned editor is generally an inappropriate option for longer forms.
|
Note
|
Small Viewports
On small viewports like mobile phones, the editor always opens as a full-screen overlay, regardless of this configuration.
|
Editor Content
The editor’s content is fully configurable, except for the header and footer.
Source code
CrudEditorContent.java
PersonDataProvider.java
PersonDataProvider.java
crud-editor-content.tsx
crud-editor-content.ts
Editor Actions
The editor contains three Buttons: Delete, Cancel, and Save. The Delete shows a confirmation dialog asking the user to verify whether they wish to delete the item. Whereas the Cancel closes the editor unless there are unsaved changes. If so, a confirmation dialog is shown and the user can either discard the changes or go back to editing. The Save button, when clicked, saves the changes and closes the editor. This is disabled until a change is made.
Save Button State Flow
Save is enabled as soon as the editor becomes dirty — that is, as soon as the user changes the value of a field in it. Validity doesn’t factor into this: Save is enabled for invalid input, too. Clicking it runs the editor’s validate() method, and when validation fails, the editor stays open and nothing is saved.
An editor that doesn’t propagate its field changes — one built from a composite component that wraps its fields, for example — can leave Save permanently disabled. Call setDirty(true) on the CRUD to enable it explicitly in such a case.
Some applications need Save to be enabled at all times. A common accessibility pattern is to let the user submit at any point and then show a summary of what needs fixing. Set the enabled state of the Save Button directly to get this: CRUD stops managing that Button’s state from then on, leaving it enabled regardless of whether the editor is dirty. The editor’s validate() method still decides whether a save goes through.
Source code
Java
crud.getSaveButton().setEnabled(true);See Custom Editor for an editor that pairs this with an error summary.
Editor Button Access Flow
CRUD’s Buttons are ordinary Button instances that you can access from the server to change their state, appearance, or behavior:
| Method | Description |
|---|---|
| The editor’s Save Button. Setting its enabled state hands its management over to you — see Save Button State. |
| The editor’s Cancel Button. |
| The editor’s Delete Button. CRUD hides it automatically while a new item is being created. |
| The toolbar’s New item Button. See Toolbar for an example of replacing it. |
Set the Button labels through Localization, not setText(): CRUD writes the localized labels onto its default Buttons, overwriting anything set that way.
Some datasets shouldn’t allow deletion at all — for example, records that are archived or deactivated instead of removed, so that history is preserved. There’s no API for removing the Delete Button, so the only way to do this at present is to hide it with CSS.
Source code
CrudEditorButtons.java
|
Note
|
Hiding Delete Is a Workaround
Setting display: none isn’t advocated by the CRUD API; it’s the only thing that works today. Don’t reach for setVisible(false): CRUD manages the Delete Button’s hidden attribute itself, and it clears the attribute each time the editor opens for an existing item, which makes the Button reappear.
|
Custom Editor Flow
BinderCrudEditor is the stock editor implementation, but it isn’t the only option. CrudEditor is a public interface, so you can supply any implementation — with its own layout, its own state handling, and its own validation — through the Crud constructor or setEditor().
CRUD calls the interface’s methods at these points:
| Method | When It’s Called |
|---|---|
| When the editor is opened, for a new or an existing item. The second parameter tells the editor whether to validate the item immediately. |
| Whenever CRUD needs the item currently being edited, such as when it builds a save or delete event. |
| When Save is clicked. Returning |
| After |
| When the editor is closed, whether by saving, deleting, or cancelling. |
| When the editor is set, to get the form to place inside CRUD. |
The editor in the example below validates the item itself and renders all problems in a summary at the top of the form. It’s paired with an always-enabled Save Button, so the user can submit at any point and be told what’s missing.
Source code
CrudCustomEditor.java
PersonCrudEditor.java
PersonCrudEditor.java
Controlling the Editor Programmatically Flow
The editor doesn’t have to be opened by the user. These methods drive it from the server:
| Method | Description |
|---|---|
| Opens the editor for an item that’s already in the dataset. |
| Opens the editor for a new item, which is what the New item Button does. See Toolbar for an example. |
| Opens or closes the editor without changing the item being edited. |
| Sets where the editor is rendered: |
| Opens the editor when the user clicks a row, instead of requiring the edit Button. This removes the edit column from CRUD’s built-in grid. |
Grid Replacement
CRUD’s default Grid is replaceable, which is useful when you wish to customize the Grid. An example of this might be if you want to place the edit Button in the first column, or to apply tooltips. See Grid documentation for details on configuring grids.
Source code
CrudGridReplacement.java
PersonDataProvider.java
PersonDataProvider.java
crud-grid-replacement.tsx
crud-grid-replacement.ts
|
Note
|
Edit Column
You need to add explicitly an edit column to the replacement Grid to be able to edit items. Additionally, Grid doesn’t have sorting and filtering enabled by default.
|
Toolbar
Creating new items is done via the “New Item” Button in CRUD’s toolbar. Both the toolbar and its Button are customizable. For example, you can use the toolbar to display statistics such as the size of the dataset or the number of search results.
Source code
CrudToolbar.java
PersonDataProvider.java
PersonDataProvider.java
crud-toolbar.tsx
crud-toolbar.ts
Hiding the Toolbar
The toolbar can be hidden if it isn’t needed.
Source code
CrudHiddenToolbar.java
crud-hidden-toolbar.tsx
crud-hidden-toolbar.ts
Sorting & Filtering
By default, CRUD allows sorting and filtering of any column. For more information about sorting and filtering, see the Grid documentation.
|
Note
|
No Manual Reordering
CRUD sorts and filters, but it doesn’t support manual reordering — letting the user drag rows or move them with up and down controls to set a display order of their own. If you need that, use Grid Replacement and add the reordering controls to the replacement Grid.
|
Disabling Sorting & Filtering
Sorting and filtering can be disabled.
Source code
CrudSortingFiltering.java
PersonDataProvider.java
PersonDataProvider.java
crud-sorting-filtering.tsx
crud-sorting-filtering.ts
Lazy Backend Loading Flow
CRUD’s built-in grid passes the user’s sorting and filtering to the data provider as a CrudFilter, so that the backend can do the work instead of the server holding the full dataset in memory. A data provider used with that grid has to accept this filter type: CrudGrid throws an IllegalArgumentException for anything that isn’t a DataProvider<E, CrudFilter>.
CrudFilter carries two maps, both keyed by column key:
-
getConstraints()maps a column to the text the user typed into its filter field. Translate these into aWHEREclause. -
getSortOrders()maps a column to aSortDirection. Translate these into anORDER BYclause.
The filter is handed to the data provider on every fetch, and CRUD refreshes the grid whenever the user changes a filter field or a sort order. The PersonDataProvider used in the examples on this page shows the full pattern: it extends AbstractBackEndDataProvider<Person, CrudFilter>, converts the constraints into a predicate and the sort orders into a comparator, and applies the offset and limit from the query.
Source code
PersonDataProvider.java
package com.vaadin.demo.component.crud;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.crud.CrudFilter;
import com.vaadin.flow.data.provider.AbstractBackEndDataProvider;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.data.provider.SortDirection;
import static java.util.Comparator.naturalOrder;
// Person data provider
public class PersonDataProvider
extends AbstractBackEndDataProvider<Person, CrudFilter> {
// A real app should hook up something like JPA
final List<Person> DATABASE = new ArrayList<>(DataService.getPeople());
private Consumer<Long> sizeChangeListener;
@Override
protected Stream<Person> fetchFromBackEnd(Query<Person, CrudFilter> query) {
int offset = query.getOffset();
int limit = query.getLimit();
Stream<Person> stream = DATABASE.stream();
if (query.getFilter().isPresent()) {
stream = stream.filter(predicate(query.getFilter().get()))
.sorted(comparator(query.getFilter().get()));
}
return stream.skip(offset).limit(limit);
}
@Override
protected int sizeInBackEnd(Query<Person, CrudFilter> query) {
// For RDBMS just execute a SELECT COUNT(*) ... WHERE query
long count = fetchFromBackEnd(query).count();
if (sizeChangeListener != null) {
sizeChangeListener.accept(count);
}
return (int) count;
}
void setSizeChangeListener(Consumer<Long> listener) {
sizeChangeListener = listener;
}
private static Predicate<Person> predicate(CrudFilter filter) {
// For RDBMS just generate a WHERE clause
return filter.getConstraints().entrySet().stream()
.map(constraint -> (Predicate<Person>) person -> {
try {
Object value = valueOf(constraint.getKey(), person);
return value != null && value.toString().toLowerCase()
.contains(constraint.getValue().toLowerCase());
} catch (Exception e) {
e.printStackTrace();
return false;
}
}).reduce(Predicate::and).orElse(e -> true);
}
private static Comparator<Person> comparator(CrudFilter filter) {
// For RDBMS just generate an ORDER BY clause
return filter.getSortOrders().entrySet().stream().map(sortClause -> {
try {
Comparator<Person> comparator = Comparator.comparing(
person -> (Comparable) valueOf(sortClause.getKey(),
person));
if (sortClause.getValue() == SortDirection.DESCENDING) {
comparator = comparator.reversed();
}
return comparator;
} catch (Exception ex) {
return (Comparator<Person>) (o1, o2) -> 0;
}
}).reduce(Comparator::thenComparing).orElse((o1, o2) -> 0);
}
private static Object valueOf(String fieldName, Person person) {
try {
Field field = Person.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(person);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
void persist(Person item) {
if (item.getId() == null) {
item.setId(DATABASE.stream().map(Person::getId).max(naturalOrder())
.orElse(0) + 1);
}
final Optional<Person> existingItem = find(item.getId());
if (existingItem.isPresent()) {
int position = DATABASE.indexOf(existingItem.get());
DATABASE.remove(existingItem.get());
DATABASE.add(position, item);
} else {
DATABASE.add(item);
}
}
Optional<Person> find(Integer id) {
return DATABASE.stream().filter(entity -> entity.getId().equals(id))
.findFirst();
}
void delete(Person item) {
DATABASE.removeIf(entity -> entity.getId().equals(item.getId()));
}
}PersonDataProvider.java
|
Note
|
Honoring the Filter
The data provider is responsible for applying the filter. If fetchFromBackEnd() ignores it, the grid’s filter fields and sort indicators still appear, but using them has no effect.
|
Item Initialization
Newly created items can be initialized with data.
Source code
CrudItemInitialization.java
crud-item-initialization.tsx
crud-item-initialization.ts
Localization
CRUD supports full localization through customizable labels for its buttons and the title of the editor.