Skip to content

Pagination

Natan Vieira edited this page Dec 9, 2023 · 189 revisions

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

Creating Pagination

To create a pagination you have two essential parameters:

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

Use paginationState to create a new static pagination.
(There are other types of pagination that will be explained later)

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: what 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

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


With user provided layout in the configuration
Respects the position specified by "O" character in the layout.

Preview

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

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 given for the standard library: the behavior of such API may be changed or the API may be removed completely in any further release.

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))
        .displayIf(pagination::isLoading)
        .updateOnStateChange(paginationState);
}

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))
        .updateOnStateChange(paginationState)
        .onClick(pagination::back);

    // To advance
    render.lastSlot(new ItemStack(Material.ARROW))
        .updateOnStateChange(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))
        .updateOnStateChange(paginationState)
        .displayIf(pagination::canBack)
        .onClick(pagination::back);

    // To advance
    render.lastSlot(new ItemStack(Material.ARROW))
       .updateOnStateChange(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)
}