Pagination on Return via Browser

Hi,

i hava a lazy Grid with items and dataprovider. When i click into an item and go back ia Browser return button, i want to jump to the correct page & position.

I have tried to save this data temporaily in the session an read it in beforeenter. But anyhow it wont work. Does anyone has successfully implemented this kind of feature and could guide me in the right direction?

Thanks a lot!

This happens because the scroll position is not preserved when DOM elements are re-created.

One way around this is to preserve DOM elements e.g. by using the new MasterDetailLayout. Another possibility is to keep track of which item was selected before and then use scrollToItem when returning.

I use scrollToItem in most views with Grids

i have tried it but it didnt work. I think i invoked it to early (should be after dataprovider.refreshAll() probably. Need to give it another try. Thank you!

The object in grid must override egauls () and hash() to work?

This code should go into afterNavigation.

1 Like

Thank you Simon, works perfectly!

Thats a huuuuuuge improvent in UX for my users.

1 Like

@SimonMartinelli

i must use entityGrid.getLazyDataView().setItemIndexProvider right? Got an issue, when i scroll down the page and up and open some “cards” after 5 or 6 “Browser” backs, the index is wrong and it scrolls to a incorrect entry. Looks like the index is incorrect.

When i click a Card Item in Grid i add the index to the browser session:

Code
public OverviewCard(OrderEntity orderEntity, int index, AtomicInteger indexCounter) {
        addClassNames("order-card", LumoUtility.Display.FLEX, LumoUtility.FlexWrap.WRAP);
        setJustifyContentMode(JustifyContentMode.EVENLY);
        setPadding(true);
        setSpacing(true);
            createOrderCard(orderEntity);
            addClickListener(event -> {
                UI.getCurrent().getPage().executeJs("sessionStorage.setItem($0, $1);", "selectedItem", orderEntity.getId());
                UI.getCurrent().getPage().executeJs("sessionStorage.setItem($0, $1);", "selectedItemIndex", index);
                UI.getCurrent().getPage().executeJs("sessionStorage.setItem($0, $1);", "selectedItemOffset", indexCounter.get());

         //Navigate to the URL which routes to the corresponding DetailsItem
                );
            });
    }

when i come back into the overview, i read the index etc vfrom session, that works some rounds but like mentioned after 5 or 6 tests, it scrolls to a false index

Code
@Override
    public void afterNavigation(AfterNavigationEvent event) {
        UI ui = UI.getCurrent();

        ui.getPage().executeJs("return sessionStorage.getItem($0);", "selectedItem")
                .then(String.class, selectedIdStr -> {
                    if (selectedIdStr == null) return;

                    ui.getPage().executeJs("return sessionStorage.getItem($0);", "selectedItemIndex")
                            .then(String.class, indexStr -> {
                                if (indexStr == null) return;
                                int selectedId = Integer.parseInt(selectedIdStr);
                                int indexOnPage = Integer.parseInt(indexStr);

                                OrderEntity orderEntity = orderService.findOrderById(selectedId).orElse(null);
                                if (orderEntity != null) {
                                    entityGrid.getLazyDataView().setItemIndexProvider((order, query) -> {
                                        return order.getId() == selectedId ? indexOnPage : -1;
                                    });
                                    entityGrid.scrollToItem(orderEntity);
                                }

                            });
                });
    }

I bet there is a way easier way! That looks really to overbloated.

You can just keep the selected item in a SessionScoped bean and then use
entityGrid.scrollToItem

unfortunately its not that easy.

i get:

getItemIndex method in the LazyDataView requires a callback to fetch the index. Set it with setItemIndexProvider.

if i only have it like this (vaadinSessionData is my sessionScoped Bean)

  @Override
    public void afterNavigation(AfterNavigationEvent event) {
        if(vaadinSessionData.getSelectedOrderEntity()!=null){
            entityGrid.scrollToItem(vaadinSessionData.getSelectedOrderEntity());
        }
    }

But i only have a dataprovider which looks like not handling the index of each entitiy

protected CallbackDataProvider<OrderEntity, Void> createPagedDataProvider() {
        return new CallbackDataProvider<>(
                query -> {
                    CustomFilter filter = new CustomFilter();
                   //filter config

                    List<OrderEntity> orders = orderDataProvider
                            .fetchFromBackEnd(new Query<>(query.getOffset(), query.getLimit(), query.getSortOrders(), null, filter))
                            .limit(50)
                            .collect(Collectors.toList());

                    return orders.isEmpty()
                            ? Stream.of(createEmptyOrder())
                            : orders.stream();
                },
                query -> {
                    CustomFilter filter = new CustomFilter();
                  //filter

                    int count = orderDataProvider.sizeInBackEnd(new Query<>(filter));
                    updateTotalOrdersCount(count);
                    return Math.max(count, 1);
                }
        );
    }

what’s that?
Why don’t you use setItems?

What the hell :D Thats a reall good question! I used

entityGrid.setDataProvider(createPagedDataProvider());

(but dont know why)

but when i use
entityGrid.setItems(createPagedDataProvider());

it looks like it works out of the box without additional index handling?

You don’t even need to create the data provider. Just use setItemes and pass two lambdas

Even when i use setItems with two lambdas i get the error message

getItemIndex method in the LazyDataView requires a callback to fetch the index. Set it with setItemIndexProvider.

        entityGrid.setItems(
                query -> fetchItems(query),
                query -> countItems()
        );

Do you have any idea? As far as i can find any info (as well ChatGPT says i need it), i need to implement kind of:

LazyDataView<OrderEntity> lazyDataView = grid.setItems(fetchItems(query));
lazyDataView.setItemIndexProvider(item -> {
//fetch index from database
    return orderDataProvider.getIndexOf(item);
});

Oh, you are right.

I found the hack we used:

Person person = new Person(357, "Person 357");
Stream<Person> items = grid.getGenericDataView().getItems();
AtomicInteger i = new AtomicInteger();
int index = items.peek(v -> i.incrementAndGet()).anyMatch(itm -> itm.equals(person)) ? i.get() - 1 : -1;
grid.scrollToIndex(index);
grid.select(person);

Thanks for checking. Is that in your afterNavigation()?

yes. This will call the dataprovider fetch until it reaches the person requested

1 Like

Thanks, i will give it a try tomorrow. Hope it will work otherwise i need to go the way with the additional query

I would like to summon @Leif to this discussion combined with the following comment:

We currently have an open VS-(number not with me) about a requirement to re-select/scroll-to a Combobox value after re-opening. This Grid example looks like the exact same use-case where a flow solution / improvement on the data provider API might be a good fit.

The point of that additional query is performance with large data sets. If you have 100000 items in the Grid and want to find the index of an item that is at the very end, then you’d end up loading all of those items into memory (or doing n + 1 requests to avoid loading the very last one) if you don’t have that additional query. The typical query is also not efficient if it has to do a full table scan in the database but that’s still way better than the in-memory approach that scans the whole database while also allocating all those objects on the Java heap.

(And I suspect Simon’s hack could be made slightly more pretty by using something like items.takeWhile(item -> !item.equals(person)).count(); to avoid the AtomicInteger)

What might be useful in both cases could be a feature to get the current scroll position out of the component before navigating away so that you can easily restore that exact position when returning without any of the indirections discussed here. The main challenge there is that you don’t want to send all those intermediate scroll events to the server but somehow only the result of the very last one.

One generic framework mechanism for that could be a “scroll preserver” that allows you to associate a unique id for some scrollable container. There would then be a client-side scroll listener that stores the scroll position in a client-side map (potentially configurable whether that’s in-memory, session storage, or local storage) and then using the same unique id when returning to automatically restore the scroll position.

Another potential generic mechanism could be a client-side “before detach” listener that can extract selected state right before a component is detached and send that to server-side application logic to use for any purpse.