Skip to content

Pagination

Natan Vieira edited this page Aug 15, 2023 · 189 revisions

Pagination is a component that allows you to display a large amount of data in an view by separating it into pages.

Creating a Pagination

To create a pagination state you have two essential parameters:

  1. sourceProvider will be used to define the pagination data, e.g.: a list of things.
  2. itemFactory will be used to create the items that will be displayed in the view for each paginated element, e.g.: a head of a player.

Use paginationState to create a new Pagination state.

private final State<Pagination> paginationState = paginationState(sourceProvider, itemFactory);
Data Source - what will be paginated

In the example below, our data to be paginated is a list of player names: Notch, Natan and Michael.

The list of players name is provided by the sourceProvider parameter and for each name we will display a head provided by the itemFactory parameter.

Preview
private final State<Pagination> paginationState = paginationState(
    Arrays.asList("Notch", "Natan", "Michael"),
    (builder, name) -> builder.withItem(new ItemStack(...))
);

Item Factory - how will be displayed to the player

The second parameter, itemFactory, is a BiConsumer<T, V> being T what you will use to define build your item, it has a set of functions like onClick, withItem, among others. We learned about it in the Basic Usage. V is the type of the element that's being paginated, in our case it's String that's the player name.

Preview
private final State<Pagination> paginationState = paginationState(
    Arrays.asList("Notch", "Natan", "Michael"),
    (builder, name) -> builder.withItem(createHead(name))
);

private ItemStack createHead(String name) { ... }

Pagination + Layouts

Layouts are a feature that we provide to your to organize the items based on a set of characters and they can work together with pagination items! Take a look on how the code of the previous topic behavis with and without a layout.

No layout provided (default behavior)


Preview

Uses the entire inventory size that's explicitly defined by *size*.

@Override
public void onInit(ViewConfigBuilder config) {
    config.size(3);
}


With layout - respects the layout bondaries

Configure the layout on initialization with layout.

Preview
@Override
public void onInit(ViewConfigBuilder config) {
    config.layout(
        "         ", 
        "  OOOOO  ", 
        "         "
    );
}

private final State paginationState = paginationState( sourceProvider, itemFactory );

Layouts are not used only in pagination so learn more about layouts on Wiki to do not get thing messy elsewhere as well!

Per-Player Data Source

By default the paging state allows providing multiple types of data source, a static data source can be defined for paging externally provided data that does not need a context, e.g. the server's warps.

final class WarpsListView extends View {

    private final State<Pagination> paginationState;

    private WarpsListView(WarpsManager warpsManager) {
        this.paginationState = paginationState(
            warpsManager.getWarps(),
            (itemBuilder, warp) -> itemBuilder.withItem(...)
        );
    }

}

Now when more context data is needed like for example: the userid, name, or anything else from the context, we start working with a per-player dynamic data source (it's per context but we'll use the term "per-player" to make it easier to understand).

The paging state provides an overload containing Function<IFContext, T> which can be used as a way to assign the paging data source using the current context as a parameter.

In the example below we will create an inventory whose pagination source will be the player's inventory items.

private final State<Pagination> paginationState = paginationState(
    (context) -> Arrays.asList(context.getPlayer().getInventory().getContents()),
    (itemBuilder, value) -> ...
);

Asynchronous Data Source

Asynchronous Pagination is experimental and is not subject to the general compatibility guarantees such functionality may be changed or may be removed completely in any further release, its behavior may be inconsistent and is subject to change.

Asynchronous pagination allows the developer to use a CompletableFuture as data source. It works like dynamic pagination but it waits for the future to complete each time the page switches.

Use asyncPaginationState/buildAsyncPaginationState to create asynchronous pagination.

private final State<Pagination> paginationState = paginationState(
    (context) -> fetchClansFromDatabase(),
    (itemBuilder, value) -> ...
);

private CompletableFuture<List<Clan>> fetchClansFromDatabase() {
    CompletableFuture<List<Clan>> task = /* ... some hard work goin on here ... */
    ...
    return task;
}

Watching for loading state of pagination

There is a property called isLoading that changes every time the task to load the pagination data is begins or completes load, for you to know if the data is being loaded or not.

You can display an item like "Data is being loaded..." based on this condition. In the example below we show a clock while the data is loading.

private final State<Pagination> paginationState = paginationState(
    (context) -> /* CompletableFuture */,
    (itemBuilder, value) -> ...
);

@Override
public void onFirstRender(RenderContext render) {
    final Pagination pagination = paginationState.get(render);

    render.lastSlot(new ItemStack(Material.CLOCK))
        .watch(paginationState)
        .displayIf(pagination::isLoading);
}

Diving into Pagination

Pagination exposes a specific type of state value called Pagination which is where the pagination data of a context is stored.

It contains several functions such as the number and index of the current page, next and previous pages, pagination data, navigation, etc. Dive into this to discover the possibilities of what to do with pagination for a context.

State<Pagination> pagination = paginationState.get(context);

Navigation

Navigation is no secret and there are some very explicitly named functions for it.

  • back() Backs to the previous page if available;
  • advance() Advances to the next page if any;
  • switchTo(n) Jumps to a specific page index.

In the example below we have "ARROW" for the "back to previous page" in the first slot and "forward to next page" in the last slot items.

@Override
public void onFirstRender(RenderContext render) {
    Pagination pagination = paginationState.get(render);

    // To back
    render.firstSlot(new ItemStack(Material.ARROW))
        .watch(paginationState)
        .onClick(pagination::back);

    // To advance
    render.lastSlot(new ItemStack(Material.ARROW))
        .watch(paginationState)
        .onClick(pagination::advance);
}

"What if I don't want to display the navigation items if there are no pages to navigate?"
For this you can use displayIf(BooleanSupplier) which was learned in the Basic Usage on Wiki.

@Override
public void onFirstRender(RenderContext render) {
    Pagination pagination = paginationState.get(render);

    // To back
    render.firstSlot(new ItemStack(Material.ARROW))
        .watch(paginationState)
        .displayIf(pagination::canBack)
        .onClick(pagination::back);

    // To advance
    render.lastSlot(new ItemStack(Material.ARROW))
       .watch(paginationState)
       .displayIf(pagination::canAdvance)
       .onClick(pagination::advance);
}

Handling Page Switch

Handling of page switching is something little used so it is not accessible directly in the quick function paginationState, it will be necessary to use the advanced pagination builder for that buildPaginationState.

private final State<Pagination> paginationState = buildPaginationState(...)
    .onPageSwitch((context, pagination) -> {
        // ... do something on page switch ...		
    })
    .build();

Multiple paginations

As stated in the Introduction, pagination is a component and as it is possible to have multiple components in the view you can have multiple paginations as well. You just need to define the target layout character for each pagination.

In the example of this topic, we will render two paginations side by side, the one on the left side as "L" and the one on the right side as "R".

private final State<Pagination> leftPaginationState = buildPaginationState(...)
    .layoutTarget('L')
    .itemFactory((item, value) -> item.withItem(new ItemStack(Material.GOLD_INGOT)))
    .build();

private final State<Pagination> rightPaginationState = buildPaginationState(...)
    .layoutTarget('R')
    .itemFactory((item, value) -> item.withItem(new ItemStack(Material.IRON_INGOT)))
    .build();

And then just create a layout that contains both "L" and "R" characters

Preview
@Override
public void onInit(ViewConfigBuilder config) {
     config.layout(
	 "         ",
	 " LLL RRR ",
	 " LLL RRR ",
	 " LLL RRR ",
	 "         "
    );
}

Navigation is done in the same way as normal pagination, add an item to it and navigate as usual.

@Override
public void onFirstRender(RenderContext render) {
    Pagination leftPagination = leftPaginationState.get(render);
    Pagination rightPagination = rightPaginationState.get(render);

    // Manipulate "L" pagination navigation
    render.slot(...).watch(leftPagination).onClick(leftPagination::back)
    render.slot(...).watch(leftPagination).onClick(leftPagination::advance)

    // Manipulate "R" pagination navigation
    render.slot(...).watch(rightPaginationState).onClick(rightPagination::back)
    render.slot(...).watch(rightPaginationState).onClick(rightPagination::advance)
}