Skip to main content
  1. Posts/

Advent Calendar - 2025 - Filter & Search – Part 02

Sven Ruppert
Author
Sven Ruppert
20+ years of Java, specialised in Security, Vaadin and Developer Relations. When not coding, you’ll find me in the woods with an axe.
Table of Contents

In the previous part, we looked at the implementation on the server side. This part is now about the illustration on the user page.

The source code for the initial state can be found on GitHub under https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-00 .

The following screenshot shows this state of development.

Screenshot of the URL Shortener overview page, displaying a table of short links with columns for Short Code, Original URL, Created On date, and action buttons for deletion.

The focus of this Advent calendar day is on the introduction of targeted filtering, search and paging functionality on the UI side. The goal is to extend the existing “Overview” view so that users can search specifically for specific shortcodes or URL fragments, restrict time periods and retrieve the results page by page. All the technical foundations for this have been laid in the previous part.

The source code for today’s articles can be found on Github under https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-01 .

The following screenshot shows the state of development that we will reach today.

Screenshot of the ‘URL Shortener’ application overview showing filtering, sorting, and pagination features for managing shortcodes and original URLs.

Integration into the Vaadin UI
#

After implementing the filtering and paging logic on the server and client side, the focus is now on integration into the user interface. The Vaadin UI acts as the visible layer of the system and is crucial to the user experience. The aim of this chapter is to show how the new functions – filtering, searching and page by page – are brought together in a modern, reactive interface.

In contrast to the previous parts, which primarily described technical structures, the focus here is on the interaction between user and system. The OverviewView serves as a central view through which users communicate directly with the API without having to know technical details.

The following sections show how the new API endpoints and client methods create a dynamic yet intuitive interface – from data binding and filter logic to visual fine-tuning and ergonomic design.

OverviewView – Filtering, Searching, and Paging in the Vaadin UI
#

With the new server and client functions, the basis for an interactive user interface is now available. This chapter describes the revised OverviewView – the central view of the Vaadin application in which all existing short links can be listed, filtered and managed.

The original version of the View only showed a complete list of all mappings. In the new version, it has been expanded into a dynamic, filterable and page-based interface that communicatesdirectly with the endpoints/listand/list/count.

Structure of the user interface
#

The OverviewView combines several input elements to capture search and filter criteria:

  • Text fields for shortcode and URL substrings (codePart, urlPart)
  • Case-sensitive checkboxes (codeCase, urlCase)
  • Date and time selection for time periods (fromDate, toDate)
  • Dropdowns for sorting (sortBy, dir)
  • Page size via a numeric input field (pageSize)

In addition, navigation buttons (Prev / Next) enable side control. All input is transferred to a UrlMappingListRequest object, which is passed to the URLShortenerClient when the view is refreshed .

Central logic: DataProvider
#

The data supply to the grid is provided by a CallbackDataProvider, which reacts dynamically to user interactions. This calls two methods of the client in the background:

  1. **list()** – loads the actual records of the current page.
  2. **listCount()** – determines the total number to control page navigation.

A simplified excerpt of the initialization:

dataProvider = new CallbackDataProvider<>(
    q -> {
        var req = buildFilter(currentPage, pageSize.getValue());
        return urlShortenerClient.list(req).stream();
    },
    q -> {
        var baseReq = buildFilter(null, null);
        totalCount = urlShortenerClient.listCount(baseReq);
        refreshPageInfo();
        return totalCount;
    }
);

This architecture enables the View to react immediately to any change in the filter without having to reload the entire page. The user can conveniently browse through the results, adjust filters or change time periods – the data is loaded live in each case.

UX Improvements
#

In addition to the functional enhancement, the user experience has also been improved:

  • Responsive layouts: All filter elements arrange themselves flexibly, even with smaller window sizes.
  • Automatic page navigation: The Prev and Next buttons are automatically deactivated depending on the context.
  • Date-time combination: Separate DatePicker and TimePicker fields keep input precise and intuitive.

These innovations lead to considerable added value in the daily use of the application: Instead of static tables, there is now an interactive, reactive interface that is both more performant and more ergonomic.

In the next section, we’ll look at the implementation of the paging and filtering logic in detail, including the buildFilter() method and how to handle the parameters in the UI.

Implementation details of the filter logic – buildFilter() and data binding
#

The buildFilter() method forms the heart of the new filter mechanics in the Vaadin interface. It collects all of the user’s input—such as text fields, checkboxes, dates, and sorting options—and transfers them to a UrlMappingListRequest object. This request object is then passed on to the client, which uses it to generate the appropriate query parameters for the server.

Structure of the buildFilter() method
#

The focus is on the builder structure of the UrlMappingListRequest. The method first reads all UI components, checks for empty fields, and creates a consistent filter object from them:

private UrlMappingListRequest buildFilter(Integer page, Integer size) {
    UrlMappingListRequest.Builder b = UrlMappingListRequest.builder();

    if (codePart.getValue() != null && !codePart.getValue().isBlank()) {
        b.codePart(codePart.getValue());
    }
    b.codeCaseSensitive(Boolean.TRUE.equals(codeCase.getValue()));

    if (urlPart.getValue() != null && !urlPart.getValue().isBlank()) {
        b.urlPart(urlPart.getValue());
    }
    b.urlCaseSensitive(Boolean.TRUE.equals(urlCase.getValue()));

    if (fromDate.getValue() != null && fromTime.getValue() != null) {
        var zdt = ZonedDateTime.of(fromDate.getValue(), fromTime.getValue(), ZoneId.systemDefault());
        b.from(zdt.toInstant());
    } else if (fromDate.getValue() != null) {
        var zdt = fromDate.getValue().atStartOfDay(ZoneId.systemDefault());
        b.from(zdt.toInstant());
    }

    if (toDate.getValue() != null && toTime.getValue() != null) {
        var zdt = ZonedDateTime.of(toDate.getValue(), toTime.getValue(), ZoneId.systemDefault());
        b.to(zdt.toInstant());
    } else if (toDate.getValue() != null) {
        var zdt = toDate.getValue().atTime(23, 59).atZone(ZoneId.systemDefault());
        b.to(zdt.toInstant());
    }

    if (sortBy.getValue() != null && !sortBy.getValue().isBlank()) b.sort(sortBy.getValue());
    if (dir.getValue() != null && !dir.getValue().isBlank()) b.dir(dir.getValue());

    if (page != null && size != null) {
        b.page(page).size(size);
    }

    return b.build();
}

The method works strictly defensively: empty fields are ignored, optional values are only set if they actually exist. The result is always a valid, well-defined request object.

Data flow between UI and server
#

The generated filter objects are passed to theURLShortenerClient via the CallbackDataProvider. This generates the query string for the HTTP request. This creates a clear, linear chain:

UI  UrlMappingListRequest  client  /list endpoint  filter  results

Every change in the UI – be it a new search, a changed page size or a date filter – automatically triggers a refresh. The Grid component then updates itself with the newly filtered data.

Advantages of encapsulation
#

This structure offers several advantages:

  • Reusability:buildFilter() encapsulates all UI logic in one method.
  • Testability: The result can be easily tested in unit tests without the need for Vaadin-specific classes.
  • Extensibility: New filter fields can be easily added as long as they are stored in the builder.

This clean decoupling keeps the Vaadin interface clutter-free, while at the same time achieving deep integration with the API. The buildFilter() method is thus the central node between user interaction and server-side data logic.

Paging and user interaction – control, display, feedback
#

The revised OverviewView offers intuitive control for page-by-page navigation through large result sets. The aim is for users to quickly see where they are in the results list, how many elements exist in total and which interactions are currently possible.

Paging elements in the UI
#

  • Buttons:Prev (to the previous page) and Next (to the next page).
  • Page size:IntegerField pageSize with min/max limits and step buttons.
  • Status display:pageInfo shows e.g. “Page 3 / 12 • 289 total”.

These elements are directly linked to the DataProvider and are updated after each query.

Changing sides and limiting
#

Clicking on Prev/Next adjusts the current page, then the View reloads the data:

prevBtn.addClickListener(e -> {
    if (currentPage > 1) {
        currentPage--;
        refresh();
    }
});

nextBtn.addClickListener(e -> {
    int size = Optional.ofNullable(pageSize.getValue()).orElse(25);
    int maxPage = Math.max(1, (int) Math.ceil((double) totalCount / size));
    if (currentPage < maxPage) {
        currentPage++;
        refresh();
    }
});

The page size directly affects the calculation of maxPage. Changes to the pageSize field reset the current page to 1 and trigger a refresh:

pageSize.addValueChangeListener(e -> {
    currentPage = 1;
    grid.setPageSize(Optional.ofNullable(e.getValue()).orElse(25));
    refresh();
});

Status Indicator Update
#

The refreshPageInfo() method synchronizes buttons and display with the current data state:

private void refreshPageInfo() {
    int size = Optional.ofNullable(pageSize.getValue()).orElse(25);
    int maxPage = Math.max(1, (int) Math.ceil((double) totalCount / size));
    currentPage = Math.min(Math.max(1, currentPage), maxPage);

    pageInfo.setText("Page " + currentPage + " / " + maxPage + " • " + totalCount + " total");
    prevBtn.setEnabled(currentPage > 1);
    nextBtn.setEnabled(currentPage < maxPage);
}

This prevents the navigation from getting into invalid states (e.g. Prev on page 1 or Next after the last page).

Interaction with list() and listCount()
#

  • Count (**listCount**): Determines the total amount (totalCount) without transferring data that does not need to be displayed.
  • Data (**list**): Loads only the currently visible subset (based on page/size and current filters).

The combination minimizes network load and ensures consistently fast response times.

UX details and fault tolerance
#

  • Disable logic: Buttons are automatically (de)activated depending on currentPage/maxPage.
  • Error handling: Network or server errors are immediately visible viaNotification.show(“Loading failed”).
  • Reset behavior: The reset button sets filters to default values, currentPage to 1 and triggers refresh().

These mechanisms create a reactive, robust paging experience that remains clear even with very large amounts of data and provides clear feedback to the user.

UX & styling – visual and ergonomic refinements
#

Now that the functionality of the filtering, search and paging mechanisms has been established, this chapter focuses on the fine-tuning of the user interface. The aim is to improve the user experience of the Vaadin interface without changing the technical structure. The focus is on clarity, responsiveness and consistent visual feedback.

Basic design principles
#

  1. Clarity over complexity – Each function (e.g., filtering, sorting, paging) should be visually recognizable without overwhelming the user with options.
  2. Visual grouping – Logically related items are grouped into horizontal or vertical layouts (e.g., time range, sorting options, search fields).
  3. Consistent feedback – Every user action (e.g., changing a filter, changing pages, deleting action) receives direct visual or textual feedback.

Exemplary adjustments
#

1. Layout structure
#

The input elements for filters and paging have been divided into several layout lines:

var filterRow = new HorizontalLayout(
    codePart, codeCase,
    urlPart, urlCase,
    fromGroup, toGroup,
    sortBy, dir
);
filterRow.setDefaultVerticalComponentAlignment(Alignment.END);

var pagingRow = new HorizontalLayout(prevBtn, nextBtn, pageInfo, pageSize);
pagingRow.setDefaultVerticalComponentAlignment(Alignment.CENTER);

add(filterRow, pagingRow, grid);

This division semantically separates filters and navigation and thus improves visual orientation.

2. Color and Theme Consistency
#

The buttons now use Vaadin’s own theme variants to convey semantic meaning:

searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
resetBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
deleteBtn.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);

This immediately signals to the user which actions are primary, neutral or destructive.

3. Responsive behavior
#

By using setWrap(true) and percentage widths, the layout is also optimally displayed on smaller screens. Filter elements automatically wrap into new lines without disturbing the overall impression.

filterRow.setWrap(true);
filterRow.setWidthFull();
4. Interactive status messages
#

Error and success messages are communicated via Notification.show(). In addition, the affected element can optionally be visually marked – for example, by means of temporary color changes or tooltips.

Notification.show("Short link deleted.");

Bringing together function and aesthetics
#

These design and ergonomic fine-tunes make the OverviewView a reactive tool for everyday use. The application not only appears technically sophisticated, but also visually conveys the demand for precision and quality.

This completes the implementation of the UI for the new filter and search logic – a basis on which later extensions such as caching, auto-suggest or extended sorting logics can be built.

Conclusion and outlook
#

With the implementation of the filter, search and paging functions, the first milestone of the Advent calendar project has been reached. A static overview has become a dynamic, interactive system that allows data to be accessed in a targeted and efficient manner. The basic idea of the project – a solution implemented entirely in Core Java without external frameworks – has been consistently retained.

Review
#

This day showed how a clear separation of responsibilities and clean API design can create a solid basis for more complex functionalities:

  • Server-side : Introduction of the UrlMappingFilter and expansion of the InMemoryUrlMappingStore for structured queries.
  • Client-side : Extension of the URLShortenerClient with type-safe methods for filtering and counting.
  • UI side : Integration of the new features into the Vaadin-based interface with reactive paging, intuitive navigation and clear user guidance.

The result is a consistent overall system that remains modular, expandable and testable.

Cheers Sven

Related

Introduction to the URL‑Shortener Advent Calendar 2025

December 2025 is all about a project that has grown steadily in recent months: the Java-based URL Shortener, an open-source project implemented entirely with Core Java, Jetty, and Vaadin Flow. The Advent calendar accompanies users every day with a new feature, a technical deep dive, or an architectural improvement – from the basic data structure and REST handlers to UI components and security aspects.

How and why to use the classic Observer pattern in Vaadin Flow

1. Introduction and motivation # The observer pattern is one of the basic design patterns of software development and is traditionally used to decouple state changes and process them. Its origins lie in the development of graphical user interfaces, where a shift in the data model required synchronising several views immediately, without a direct link between these views. This pattern quickly established itself as the standard solution to promote loosely coupled architectures.

What makes Vaadin components special?

·6 mins
From my experience, Vaadin has always stood out from other Java frameworks. Of course, it enables the creation of modern web UIs, but the real difference lies in its component architecture. This is not conceived as a short-term aid, but is consistently designed for maintainability and flexibility. It creates the possibility of running applications stably for many years while still being able to extend them step by step.

Part III - WebUI with Vaadin Flow for the URL Shortener

·19 mins
1. Introduction and objectives # The first two parts of this series established the theoretical and practical foundations of a URL shortener in pure Java. We discussed the semantic classification of short URLs, the architecture of a robust mapping system, and the implementation of a REST-based service based on the JDK HTTP server. These efforts resulted in a functional, modularly extensible backend that creates, manages, and efficiently resolves short links. However, a crucial component was missing-a visual interface for direct user interaction with the system. This interface is essential for tasks such as manual link creation, viewing existing mappings, and analysing individual redirects.