Created
May 8, 2025 07:03
-
-
Save TatuLund/0c79760bc1115cfae573e069414fab41 to your computer and use it in GitHub Desktop.
There is no paged Grid variant in Vaadin framework yet. However it is possible to do it using custom data provider. This is an example of wrapping paged data provider in GridPager utility class which can be used with given Grid. This makes the utility general purpose and can be used with extended Grid components such as GridPro, SelectionGrid an…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.example.application.views; | |
| import com.vaadin.flow.component.Component; | |
| import com.vaadin.flow.component.button.Button; | |
| import com.vaadin.flow.component.button.ButtonVariant; | |
| import com.vaadin.flow.component.grid.Grid; | |
| import com.vaadin.flow.component.html.Span; | |
| import com.vaadin.flow.component.icon.Icon; | |
| import com.vaadin.flow.component.icon.VaadinIcon; | |
| import com.vaadin.flow.component.orderedlayout.HorizontalLayout; | |
| import com.vaadin.flow.component.orderedlayout.VerticalLayout; | |
| import com.vaadin.flow.component.select.Select; | |
| import com.vaadin.flow.component.select.SelectVariant; | |
| import com.vaadin.flow.component.textfield.IntegerField; | |
| import com.vaadin.flow.component.textfield.TextField; | |
| import com.vaadin.flow.component.textfield.TextFieldVariant; | |
| import com.vaadin.flow.data.provider.CallbackDataProvider; | |
| import com.vaadin.flow.data.provider.CallbackDataProvider.CountCallback; | |
| import com.vaadin.flow.data.provider.CallbackDataProvider.FetchCallback; | |
| import com.vaadin.flow.data.provider.ConfigurableFilterDataProvider; | |
| import com.vaadin.flow.data.provider.DataProvider; | |
| import com.vaadin.flow.data.provider.Query; | |
| import com.vaadin.flow.data.value.ValueChangeMode; | |
| import com.vaadin.flow.router.Route; | |
| import com.vaadin.flow.theme.lumo.LumoUtility; | |
| import java.util.List; | |
| import java.util.Optional; | |
| import java.util.stream.Stream; | |
| @Route(value = "grid-manual-pagination", layout = MainLayout.class) | |
| public class GridManualPagination extends VerticalLayout { | |
| private DataSource dataSource; | |
| private TextField searchField = new TextField(); | |
| public GridManualPagination(PersonService personService) { | |
| dataSource = new DataSource(personService); | |
| setPadding(false); | |
| Grid<Person> grid = new Grid<>(Person.class, false); | |
| grid.addColumn(Person::getFirstName).setHeader("First name"); | |
| grid.addColumn(Person::getLastName).setHeader("First name"); | |
| grid.addColumn(Person::getEmail).setHeader("Email"); | |
| var gridWithPaginationLayout = new GridPager<Person, String>(grid, | |
| query -> dataSource.fetch(query.getFilter(), query.getOffset(), | |
| query.getLimit()), | |
| query -> dataSource.count(query.getFilter())); | |
| var dataProvider = gridWithPaginationLayout.getDataProvider(); | |
| searchField.setWidth("50%"); | |
| searchField.setPlaceholder("Search"); | |
| searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); | |
| searchField.setValueChangeMode(ValueChangeMode.EAGER); | |
| searchField.addValueChangeListener(e -> { | |
| // setFilter will refresh the data provider and trigger data | |
| // provider fetch / count queries | |
| dataProvider.setFilter(e.getValue()); | |
| }); | |
| add(searchField, gridWithPaginationLayout); | |
| } | |
| public static class GridPager<T, F> extends VerticalLayout { | |
| private PaginationControls paginationControls = new PaginationControls(); | |
| private FetchCallback<T, F> fetchCallback; | |
| private CountCallback<T, F> countCallback; | |
| private ConfigurableFilterDataProvider<T, Void, F> dataProvider; | |
| @SuppressWarnings("unchecked") | |
| private final CallbackDataProvider<T, F> pagingDataProvider = DataProvider | |
| .fromFilteringCallbacks(query -> { | |
| // Prevent IllegalStateException | |
| query.getLimit(); | |
| query.getOffset(); | |
| // determine the offset and limit for the current page. | |
| var offset = paginationControls.calculateOffset(); | |
| var limit = paginationControls.getPageSize(); | |
| Query<T, F> pagedQuery = new Query(offset, limit, | |
| query.getSortOrders(), null, | |
| query.getFilter().orElse(null)); | |
| return fetchCallback.fetch(pagedQuery); | |
| }, query -> { | |
| // Total count of filtered items | |
| var itemCount = countCallback.count(query); | |
| // Update total item count here to avoid calling | |
| // dataSource.count twice | |
| paginationControls.update(itemCount); | |
| var offset = paginationControls.calculateOffset(); | |
| var limit = paginationControls.getPageSize(); | |
| // Return the number of items for the current page, taking | |
| // the | |
| // remaining items | |
| // on the last page into consideration | |
| var remainingItemsCount = itemCount - offset; | |
| return Math.min(remainingItemsCount, limit); | |
| }); | |
| public GridPager(Grid<T> grid, FetchCallback<T, F> fetchCallback, | |
| CountCallback<T, F> countCallback) { | |
| this.fetchCallback = fetchCallback; | |
| this.countCallback = countCallback; | |
| grid.setAllRowsVisible(true); // this will prevent scrolling in the | |
| // grid | |
| dataProvider = pagingDataProvider.withConfigurableFilter(); | |
| grid.setDataProvider(dataProvider); | |
| paginationControls | |
| .onPageChanged(() -> grid.getDataProvider().refreshAll()); | |
| setPadding(false); | |
| setSpacing(false); | |
| getThemeList().add("spacing-xs"); | |
| add(grid, paginationControls); | |
| } | |
| public ConfigurableFilterDataProvider<T, Void, F> getDataProvider() { | |
| return dataProvider; | |
| } | |
| } | |
| public static class DataSource { | |
| private List<Person> people; | |
| public DataSource(PersonService personService) { | |
| people = personService.fetchAll(); | |
| } | |
| public Stream<Person> fetch(Optional<String> searchTerm, int offset, | |
| int limit) { | |
| // emulate accessing the backend datasource - in a real application | |
| // this would | |
| // call, for example, an SQL query, passing an offset and a limit to | |
| // the query | |
| return people.stream().filter( | |
| person -> matchesSearchTerm(person, searchTerm.orElse(""))) | |
| .skip(offset).limit(limit); | |
| } | |
| public int count(Optional<String> searchTerm) { | |
| return (int) people.stream().filter( | |
| person -> matchesSearchTerm(person, searchTerm.orElse(""))) | |
| .count(); | |
| } | |
| public boolean matchesSearchTerm(Person person, String searchTerm) { | |
| return searchTerm == null || searchTerm.isEmpty() | |
| || person.getFirstName().toLowerCase() | |
| .contains(searchTerm.toLowerCase()) | |
| || person.getLastName().toLowerCase() | |
| .contains(searchTerm.toLowerCase()) | |
| || person.getEmail().toLowerCase() | |
| .contains(searchTerm.toLowerCase()); | |
| } | |
| } | |
| public static class PaginationControls extends HorizontalLayout { | |
| private int totalItemCount = 0; | |
| private int pageCount = 1; | |
| private int pageSize = 10; | |
| private int currentPage = 1; | |
| private final Span currentPageLabel = currentPageLabel(); | |
| private final Button firstPageButton = firstPageButton(); | |
| private final Button lastPageButton = lastPageButton(); | |
| private final Button goToPreviousPageButton = goToPreviousPageButton(); | |
| private final Button goToNextPageButton = goToNextPageButton(); | |
| private Component createPageSizeField() { | |
| Select<Integer> select = new Select<>(); | |
| select.addThemeVariants(SelectVariant.LUMO_SMALL); | |
| select.getStyle().set("--vaadin-input-field-value-font-size", | |
| "var(--lumo-font-size-s)"); | |
| select.setWidth("4.8rem"); | |
| select.setItems(10, 15, 25, 50, 100); | |
| select.setValue(pageSize); | |
| select.addValueChangeListener(e -> { | |
| pageSize = e.getValue(); | |
| updatePageCount(); | |
| }); | |
| var label = new Span("Page size"); | |
| label.setId("page-size-label"); | |
| label.addClassName(LumoUtility.FontSize.SMALL); | |
| select.setAriaLabelledBy("page-size-label"); | |
| final HorizontalLayout layout = new HorizontalLayout( | |
| Alignment.CENTER, label, select); | |
| layout.setSpacing(false); | |
| layout.getThemeList().add("spacing-s"); | |
| return layout; | |
| } | |
| public Component createPageNumberField() { | |
| var pageNumber = new IntegerField(); | |
| pageNumber.setManualValidation(true); | |
| pageNumber.addValueChangeListener(e -> { | |
| var newPage = e.getValue(); | |
| if (newPage != null && newPage >= 0 && newPage <= pageCount) { | |
| pageNumber.setInvalid(false); | |
| currentPage = e.getValue(); | |
| firePageChangedEvent(); | |
| } else { | |
| pageNumber.setInvalid(true); | |
| } | |
| }); | |
| pageNumber.addThemeVariants(TextFieldVariant.LUMO_SMALL); | |
| pageNumber.getStyle().set("--vaadin-input-field-value-font-size", | |
| "var(--lumo-font-size-s)"); | |
| pageNumber.setWidth("4.8rem"); | |
| var label = new Span("Page number"); | |
| label.setId("page-size-label"); | |
| label.addClassName(LumoUtility.FontSize.SMALL); | |
| pageNumber.setAriaLabelledBy("page-size-label"); | |
| final HorizontalLayout layout = new HorizontalLayout( | |
| Alignment.CENTER, label, pageNumber); | |
| layout.setSpacing(false); | |
| layout.getThemeList().add("spacing-s"); | |
| return layout; | |
| } | |
| private Runnable pageChangedListener; | |
| public PaginationControls() { | |
| setDefaultVerticalComponentAlignment(Alignment.CENTER); | |
| setSpacing("0.3rem"); | |
| setWidthFull(); | |
| addToStart(createPageSizeField(), createPageNumberField()); | |
| addToEnd(firstPageButton, goToPreviousPageButton, currentPageLabel, | |
| goToNextPageButton, lastPageButton); | |
| } | |
| private void update(int totalItemCount) { | |
| this.totalItemCount = totalItemCount; | |
| updatePageCount(); | |
| } | |
| private void updatePageCount() { | |
| if (totalItemCount == 0) { | |
| this.pageCount = 1; // we still want to display one page even | |
| // though there are no items | |
| } else { | |
| this.pageCount = (int) Math | |
| .ceil((double) totalItemCount / pageSize); | |
| } | |
| if (currentPage > pageCount) { | |
| currentPage = pageCount; | |
| } | |
| updateControls(); | |
| firePageChangedEvent(); | |
| } | |
| public int getPageSize() { | |
| return pageSize; | |
| } | |
| public int calculateOffset() { | |
| return (currentPage - 1) * pageSize; | |
| } | |
| private void updateControls() { | |
| currentPageLabel.setText( | |
| String.format("Page %d of %d", currentPage, pageCount)); | |
| firstPageButton.setEnabled(currentPage > 1); | |
| lastPageButton.setEnabled(currentPage < pageCount); | |
| goToPreviousPageButton.setEnabled(currentPage > 1); | |
| goToNextPageButton.setEnabled(currentPage < pageCount); | |
| } | |
| private Button firstPageButton() { | |
| return createIconButton(VaadinIcon.ANGLE_DOUBLE_LEFT, | |
| "Go to first page", () -> currentPage = 1); | |
| } | |
| private Button lastPageButton() { | |
| return createIconButton(VaadinIcon.ANGLE_DOUBLE_RIGHT, | |
| "Go to last page", () -> currentPage = pageCount); | |
| } | |
| private Button goToNextPageButton() { | |
| return createIconButton(VaadinIcon.ANGLE_RIGHT, "Go to next page", | |
| () -> currentPage++); | |
| } | |
| private Button goToPreviousPageButton() { | |
| return createIconButton(VaadinIcon.ANGLE_LEFT, | |
| "Go to previous page", () -> currentPage--); | |
| } | |
| private Span currentPageLabel() { | |
| var label = new Span(); | |
| label.addClassNames(LumoUtility.FontSize.SMALL, | |
| LumoUtility.Padding.Horizontal.SMALL); | |
| return label; | |
| } | |
| private Button createIconButton(VaadinIcon icon, String ariaLabel, | |
| Runnable onClickListener) { | |
| Button button = new Button(new Icon(icon)); | |
| button.addThemeVariants(ButtonVariant.LUMO_ICON, | |
| ButtonVariant.LUMO_SMALL); | |
| button.addClickListener(e -> { | |
| onClickListener.run(); | |
| updateControls(); | |
| firePageChangedEvent(); | |
| }); | |
| button.setAriaLabel(ariaLabel); | |
| return button; | |
| } | |
| private void firePageChangedEvent() { | |
| if (pageChangedListener != null) { | |
| pageChangedListener.run(); | |
| } | |
| } | |
| public void onPageChanged(Runnable pageChangedListener) { | |
| this.pageChangedListener = pageChangedListener; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment