Advent Calendar 2025 – Extracting Components – Part 1
Today marks a crucial step in the evolution of the URL shortener’s user interface. After the focus in the past few days was mainly on functional enhancements – from filter and search functions to bulk operations – this day is dedicated to a structural fine-tuning: the refactoring of central UI components. This refactoring not only serves to clean up the code but also creates a clear, modular basis for future extensions as well as a significantly improved developer experience.
The source code for this article can be found on GitHub at: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-11
Here is a screenshot of the current development state from the user’s perspective.



The focus is on dividing the previously monolithic OverviewView into clearly defined, reusable components such as the BulkActionsBar and the SearchBar. This separation improves maintainability, increases clarity and makes it easier to test individual functional areas. At the same time, this will pave the way for further UI optimisations and interaction patterns to be implemented gradually in the coming days.
Motivation for refactoring
As an application’s functionality increases, so does its source code complexity. Especially in a UI that is continuously expanded and adapted to new requirements, this quickly leads to monolithic structures in which presentation, logic and state management are closely interwoven. This is precisely the situation that had developed in the OverviewView over the past few days of development: a central view that had to take on more and more tasks and thus became increasingly difficult to understand, extend, and test.
Refactoring today, therefore, starts at a fundamental point. The goal is not to introduce new features, but to create a clear, modular foundation that makes future expansions much easier. Individual UI building blocks are removed from the overloaded view and formed into independent components, with responsibility-specific tasks, clear communication channels, and significantly improved reusability. This not only reduces the complexity within the view itself, but also makes the entire architecture more robust to change.
At the same time, this refactoring strengthens the developer experience: The structure of the code becomes more comprehensible, components can be improved in isolation, and future changes – such as to the search logic or bulk operations – can be made in the right place in a targeted manner. The result is a UI design that is more stable, more flexible and easier to maintain in the long term.
Why is this step a turning point today
Today marks a critical moment in the development of the URL shortener’s user interface, as the focus shifts for the first time from new features to the code’s structural quality. While the previous steps were primarily aimed at delivering visible improvements for the user, it is now a matter of ensuring the foundation on which these functions remain solid and scalable over the long term. Such refactoring creates clarity in areas that had previously grown instead of planned – and that’s precisely what makes this day a turning point.
By separating the central UI building blocks, an architecture emerges that no longer consists of a single, heavyweight view but of clearly defined components that perform precisely defined tasks. This structural realignment opens up new possibilities for expansion, halts progress on technical debt reduction, and ensures that upcoming features no longer have to be integrated into a confusing block. Instead, each feature can be implemented in the right place without destabilising existing areas.
This step thus makes a decisive contribution to long-term development: reduced friction in the code, fewer side effects, and greater clarity. So today it’s not about visible innovations, but about the quality of the foundation – and that’s exactly what makes working on upcoming expansions much more efficient, stable and enjoyable.
Overview of the most important changes
Today, the focus is on replacing the previously central OverviewView, which had increasingly assumed responsibility over the course of development and had become correspondingly confusing. By extracting independent components, this complexity is decomposed into clearly defined building blocks.
An essential part of this restructuring is the introduction of the new BulkActionsBar, which bundles all mass operations, making both the code and the user interface more straightforward. The new SearchBar also creates a dedicated area that includes search and filter functions, making it easier to expand. Both components not only simplify the view but also provide a foundation for consistent design and recurring interaction patterns throughout the user interface.
In addition, the internal logic of the overview has been streamlined: state management, event handling, and grid interactions have been rearranged to reduce unwanted dependencies and enable targeted future changes. Minor improvements, such as the unified formatting of specific UI components, also contribute to the overall impression of a clean, structured codebase.
Why are UI components removed from views?
In mature user interfaces, a close interlocking of layout, interaction logic and state management often arises. As a result, central views become increasingly extensive over time and lose their original character. A clear entry point of the application becomes a complex block whose internal logic is difficult to understand. This is exactly where the separation of independent components comes in: It serves to clearly separate responsibilities and shift complexity to where it belongs – into small, clearly defined building blocks.
By extracting UI components, the view is reduced to its core task: orchestrating the interaction of several well-defined elements. Each component takes on a clearly defined role – be it managing search filters, triggering bulk operations, or displaying individual UI sections. This modular approach improves readability, fosters a more natural understanding of the architecture, and significantly reduces side effects during refactoring.
Another advantage is reusability. Components that are only loosely coupled to the overall view can be used flexibly elsewhere, expanded, or improved in isolation. This not only creates a robust structure but also enables more sustainable development practices, allowing new functions to be implemented without major interventions in existing areas. The separation of UI components is, therefore, an essential step in keeping a growing project stable and clear in the long term.
The new BulkActionsBar
Bulk operations are a function that plays an increasingly important role in the daily use of a URL shortener. As the number of entries grows, the need to edit several items at the same time increases – for example, to deactivate expired links, delete outdated campaigns or manage a larger number of new entries together. Such operations are not only a convenience feature but also an essential part of efficient workflows.
In many projects, bulk operations are first integrated directly into the main view. This works well at first, but in the long run, it causes view overload. Buttons, health checks, and more complex interaction logics begin to blend between Grid and View. The result is a structure that is difficult to maintain and in which extensions always carry the risk of unwanted side effects.
The decision to outsource bulk operations to a separate component creates a clear separation: the BulkActionsBar takes over the entire UI-related part of mass actions and thus forms a dedicated functional area. It bundles all relevant actions in one place, ensures a consistent user experience, and reduces complexity in the higher-level view. This clear delineation makes it much easier to add new actions, extend existing ones, or adapt the presentation to future design requirements.
In this way, outsourcing bulk operations makes a decisive contribution to a more stable and flexible architecture – and ensures that the user interface remains clear and intuitive even as requirements grow.
Structure of the BulkActionsBar component
The BulkActionsBar is an independent UI component that bundles all mass actions and presents them in a clearly structured way. Their structure follows the principle that an element should cover exactly one area of responsibility: In this case, the initiation of collection actions without itself containing logic for data processing. This keeps it easy to understand and flexible to use.
The central component of the BulkActionsBar is a clearly defined collection of interaction elements – typically buttons or icons – each of which triggers a specific action. These elements are compactly arranged in a horizontal layout, making them highly visible and intuitively accessible in the user interface. This is complemented by a mechanism that controls the visibility and activability of actions based on whether and how many entries are selected in the grid. In this way, the component not only remains clear but also guides the user through clear interaction instructions.
Another essential aspect of the structure is the abstraction of event processing. The component itself does not perform any operations; instead, it signals to the higher-level view via events or callback functions which action to trigger. This creates loose coupling between the UI and the logic, which both facilitates testing and allows adjustments to individual areas without unintentionally affecting other components.
A practical example of this is the basic structure of the class that defines the BulkActionsBar:
public class BulkActionsBar
extends Composite<HorizontalLayout>
implements HasLogger {
private final URLShortenerClient urlShortenerClient;
private final Grid<ShortUrlMapping> grid;
private final OverviewView holdingComponent;
private final Button bulkDeleteBtn = new Button(new Icon(VaadinIcon.TRASH));
private final Button bulkSetExpiryBtn = new Button(new Icon(VaadinIcon.CLOCK));
private final Button bulkClearExpiryBtn = new Button(new Icon(VaadinIcon.CLOSE_CIRCLE));
private final Button bulkActivateBtn = new Button(new Icon(VaadinIcon.PLAY));
private final Button bulkDeactivateBtn = new Button(new Icon(VaadinIcon.STOP));
private final Span selectionInfo = new Span();
public BulkActionsBar(URLShortenerClient urlShortenerClient,
Grid<ShortUrlMapping> grid,
OverviewView holdingComponent) {
this.urlShortenerClient = urlShortenerClient;
this.grid = grid;
this.holdingComponent = holdingComponent;
buildBulkBar();
addListeners();
}
}
Here, it becomes clear how the component encapsulates all elements relevant to mass actions: It knows the URLShortenerClient, the underlying Grid<ShortUrlMapping>, and the higher-level OverviewView, yet remains clearly delimited as an independent component. All buttons and the selection information area are defined within the class and are initialised in the constructor.
The actual visual structure of the BulkActionsBar is encapsulated in its own method:
private void buildBulkBar() {
--- Common style ---
bulkBar().getStyle()
.set("background", "var(--lumo-contrast-5pct)")
.set("padding", "0.4rem 0.8rem")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("border-bottom", "1px solid var(--lumo-contrast-20pct)");
--- button setup ---
setupIconButton(bulkDeleteBtn, VaadinIcon.TRASH, "Delete selected links", "var(--lumo-error-color)");
setupIconButton(bulkSetExpiryBtn, VaadinIcon.CALENDAR_CLOCK, "Set expiry for selected", "var(--lumo-primary-color)");
setupIconButton(bulkClearExpiryBtn, VaadinIcon.CALENDAR_CLOCK, "Clear expiry for selected", "var(--lumo-secondary-text-color)");
setupIconButton(bulkActivateBtn, VaadinIcon.CHECK_CIRCLE, "Activate selected", "var(--lumo-success-color)");
setupIconButton(bulkDeactivateBtn, VaadinIcon.CLOSE_CIRCLE, "Deactivate selected", "var(--lumo-error-color)");
bulkBar().removeAll();
selectionInfo.getStyle().set("opacity", "0.7");
selectionInfo.getStyle().set("font-size", "var(--lumo-font-size-s)");
selectionInfo.getStyle().set("margin-right", "var(--lumo-space-m)");
bulkBar().add(
selectionInfo,
bulkDeleteBtn,
bulkSetExpiryBtn,
bulkClearExpiryBtn,
bulkActivateBtn,
bulkDeactivateBtn
);
bulkBar().setWidthFull();
bulkBar().setSpacing(true);
bulkBar().setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
bulkBar().setVisible(false);
}
The method shows how style, layout and interaction elements are brought together in a central place. The BulkActionsBar thus defines its own visual and functional cluster within the interface, while the URLShortenerClient and the OverviewView still handle the actual execution of the actions.
Overall, this setup enables a clean separation of display and behaviour, provides a robust foundation for future expansion, and integrates seamlessly with the application’s modular architecture.
Using the BulkActionsBar in the OverviewView
The BulkActionsBar is only valuable for interaction with the OverviewView, because there it becomes clear how much outsourcing mass actions simplifies the user interface structure. While these actions were previously implemented directly in the view itself, mixing a large number of event handlers, UI elements, and logic, the BulkActionsBar now handles this entire functional area. The OverviewView only needs to define when the bar becomes visible, which entries are selected, and how to respond to the triggered actions.
The integration follows a clear pattern: As soon as the user selects one or more rows in the grid, the view displays the BulkActionsBar and passes it the current selection state. The component itself does not perform any operations, but signals which action has been triggered via clearly defined methods and events. This creates loose coupling, which increases clarity and significantly improves visibility.
Another advantage is evident when handling status changes. The OverviewView no longer has to activate or deactivate buttons individually or control complex UI dependencies. Instead, a single central feedback to the BulkActionsBar is sufficient; it updates itself. This interaction not only makes the code leaner, but also prevents errors that could easily occur with multiple scattered logics.
A central entry point for the integration is to initialise the BulkActionsBar directly as a field of the OverviewView:
private final BulkActionsBar bulkBar = new BulkActionsBar(urlShortenerClient, grid, this);
This provides the view with a fully configured instance that knows the URLShortenerClient, the grid, and the view itself. The component is then integrated into the constructor:
add(bulkBar);
add(grid);
Significant is the interaction with the grid’s selection listener. Here, the view decides whether the BulkActionsBar is visible and what information it should display:
grid.addSelectionListener(event -> {
var all = event.getAllSelectedItems();
boolean hasSelection = !all.isEmpty();
bulkBar.setVisible(hasSelection);
if (hasSelection) {
int count = all.size();
String label = count == 1 ? "link selected" : "links selected";
bulkBar.selectionInfoText(count + " " + label + " on page " + currentPage);
} else {
bulkBar.selectionInfoText("");
}
bulkBar.setButtonsEnabled(hasSelection);
});
This section clarifies the precise distribution of roles: the view recognises the current selection state, updates the visibility of the BulkActionsBar, and forwards relevant status information. The component itself remains completely free of logic for selection management.
The triggering of the actual actions also remains bundled in the view. An example of this is deleting using keyboard shortcuts:
current.addShortcutListener(_ -> {
if (!grid.getSelectedItems().isEmpty()) {
bulkBar.confirmBulkDeleteSelected();
}
},
Key.DELETE);
This once again shows the interplay of clearly defined responsibilities: the BulkActionsBar encapsulates the action UI, and the OverviewView determines the context in which it is executed.
Overall, the use of the BulkActionsBar in the OverviewView clearly demonstrates how a clear separation of responsibilities improves the architecture of a Vaadin application. The View refocuses on its core task – presenting and updating the data – while the component encapsulates and consistently delivers all the interaction around mass actions. The BulkActionsBar in the OverviewView demonstrates how a clear separation of responsibilities improves the architecture of a Vaadin application. The View refocuses on its core task – presenting and updating the data – while the component encapsulates and consistently delivers all the interaction around mass actions.
Code Comparison: Before vs. After
The difference between the previous implementation of bulk operations and the new, component-based structure is immediately apparent. Whereas previously all elements, buttons and dialogues were anchored directly in the OverviewView, distributed over numerous event handlers and UI areas, the code is now much more modular. The BulkActionsBar is a standalone component that encapsulates the entire UI for bulk actions. As a result, the OverviewView not only had to accommodate less logic but also gained overall clarity.
The difference is evident in the reduction of responsibilities within the view. Before the refactoring, the OverviewView handled all aspects: providing the buttons, displaying the bar, handling selection, opening and executing dialogues, and providing user feedback. This resulted in an extensive, tightly coupled block of code that was difficult to maintain and error-prone.
After the refactoring, the structure has improved significantly. All UI-related logic of bulk operations is now exclusively in the BulkActionsBar. The OverviewView is limited to recognising the selection and forwarding the necessary information. The component itself still triggers the actions, but the view is no longer involved in the UI’s visual or structural layout. This change improves readability, as each functional area is now located where it belongs. In addition, the risk of unintended side effects is significantly reduced, as changes to the BulkActionsBar no longer require direct intervention in the OverviewView. To clarify the difference, it is worth reviewing typical places that were previously located directly in the OverviewView and are now outsourced. A classic example is the handling of the bulk deletion operation. In the past, the OverviewView itself handled dialogue, loops, error handling, and refresh. Today, the entire logic can be found clearly closed in the BulkActionsBar:
New structure – outsourced bulk delete logic:
public void confirmBulkDeleteSelected() {
var selected = grid.getSelectedItems();
if (selected.isEmpty()) {
Notifications.noSelection();
return;
}
Dialog dialog = new Dialog();
dialog.setHeaderTitle("Delete " + selected.size() + " short links?");
var exampleCodes = selected.stream()
.map(ShortUrlMapping::shortCode)
.sorted()
.limit(5)
.toList();
if (!exampleCodes.isEmpty()) {
String preview = String.join(", ", exampleCodes);
if (selected.size() > 5) preview += ", ...";
dialog.add(new Text("Examples: " + preview));
} else {
dialog.add(new Text("Delete selected short links?"));
}
Button confirm = new Button("Delete", _ -> {
int success = 0;
int failed = 0;
for (var m : selected) {
try {
boolean ok = urlShortenerClient.delete(m.shortCode());
if (ok) success++;
else failed++;
} catch (IOException ex) {
logger().error("Bulk delete failed for {}", m.shortCode(), ex);
failed++;
}
}
dialog.close();
grid.deselectAll();
holdingComponent.safeRefresh();
Notifications.deletedAndNotDeleted(success, failed);
});
confirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY, LUMO_ERROR);
Button cancel = new Button("Cancel", _ -> dialog.close());
dialog.getFooter().add(new HorizontalLayout(confirm, cancel));
dialog.open();
}
Here you can see that the entire interaction – from the UI to the error handling to the update – now takes place within the component and no longer burdens the OverviewView.
New structure – View only signals the selection:
grid.addSelectionListener(event -> {
var all = event.getAllSelectedItems();
boolean hasSelection = !all.isEmpty();
bulkBar.setVisible(hasSelection);
if (hasSelection) {
int count = all.size();
String label = count == 1 ? "link selected" : "links selected";
bulkBar.selectionInfoText(count + " " + label + " on page " + currentPage);
} else {
bulkBar.selectionInfoText("");
}
bulkBar.setButtonsEnabled(hasSelection);
});
This shows the new role distribution: the OverviewView only controls visibility and status display. In the past, the entire operation logic would have been implemented here as well. Another example is opening the bulk set expiry dialogues. This logic is now also completely in the component and no longer distributed across the View:
bulkSetExpiryBtn.addClickListener(_ -> openBulkSetExpiryDialog());
The entire dialogue logic is then located in:
private void openBulkSetExpiryDialog() { ... }
Result:
The clear separation of responsibilities results in:
- significantly fewer lines of code in the OverviewView,
- a more structured, modular architecture,
- less coupling,
- improved maintainability and expandability.
Overall, this results in a more maintenance-friendly, extensible and clear structured architecture.
Search requirements
A practical search function is a central part of any administrative interface, especially when the number of entries is continuously growing. In the context of URL shorteners, this means that users can quickly and specifically access specific short links – regardless of whether they are looking for a particular shortcode, a URL fragment, the active status or time criteria. The previous implementation offered only simple filter options and was tightly coupled to the OverviewView, making both extensibility and maintenance difficult.
However, the requirements for a modern search component go far beyond a simple text field. It must support multiple filter criteria, respond flexibly to new fields, and adapt dynamically to the backend data structure. At the same time, it should offer the user an intuitive, consistent and clutter-free interface. This includes clear input fields, understandable labels, sensible default values, and the ability to change or reset search parameters quickly.
Technical reliability is just as crucial as user-friendliness: the search function must not burden the backend unnecessarily, support high-performance queries, and use server-side filters efficiently. A clean separation between UI, filter logic and data retrieval not only enables a better overview, but also later extensions – such as additional filter fields, sorting options or advanced functions such as combining several criteria.
The introduction of the new SearchBar addresses these requirements. It serves as the comprehensive filter and control centre for the overview and ensures that the view itself is decoupled from the filter logic. In doing so, it lays the foundation for a scalable and user-friendly search and filtering experience.
Structure and internal logic of the SearchBar
The SearchBar is designed as an independent UI component that aggregates all filter and search parameters from the OverviewView and presents them in a clearly structured format. This component aims to centralise previously scattered filter logic and significantly improve view clarity. Whereas previously individual input fields and sorting parameters were defined directly in the OverviewView, the SearchBar now assumes full responsibility for their management.
Its structure follows a modular concept: Each input – be it a text filter, a sorting criterion, the number of displayed elements or filtering by properties such as active status or expiration date – is clearly demarcated within the component and is processed independently. This not only ensures better logical separation but also enables flexible expansion with additional filters without requiring adjustments to the view itself.
The internal logic of the SearchBar works closely with the backend. Based on the user-selected parameters, the component creates structured filter objects that can be passed to the server consistently. Instead of collecting individual parameters loosely or combining them in the view, they are merged in a clearly defined process: validated, normalised, and then transferred to a backend request.
The central method buildFilter, which creates a UrlMappingListRequest object from the UI inputs, shows what this process looks like in concrete terms:
public UrlMappingListRequest buildFilter(Integer page, Integer size) {
UrlMappingListRequest.Builder b = UrlMappingListRequest.builder();
ActiveState activeStateValue = activeState.getValue();
logger().info("buildFilter - activeState == {}", activeStateValue);
if (activeStateValue != null && activeStateValue.isSet()) {
b.active(activeStateValue.toBoolean());
}
if (codePart.getValue() != null && !codePart.getValue().isBlank()) {
b.codePart(codePart.getValue());
}
if (urlPart.getValue() != null && !urlPart.getValue().isBlank()) {
b.urlPart(urlPart.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);
}
var filter = b.build();
logger().info("buildFilter - {}", filter);
return filter;
}
The code shows how all relevant filter fields – active status, shortcode and URL parts, time windows, and sorting and paging information – are consolidated in a single place. The SearchBar thus serves as a translator between UI inputs and the domain-specific filter structure in the backend. This creates a robust interface that both reduces errors and facilitates future expansions. At the same time, the SearchBar ensures a consistent user experience. Changes to a field automatically update the results list, while sensible default values and a consistent display ensure a familiar user experience. This combination of structural clarity, technical precision, and ease of use makes the SearchBar a central building block of the project’s modern UI architecture.
Improvements compared to the previous solution
From a technical standpoint, the new solution offers greater robustness. The processing is now handled entirely in the SearchBar, using structured data objects that are transmitted directly to the backend. This minimises errors caused by inconsistent or incomplete filter parameters. At the same time, the clear structure makes it easy to add new filters without jeopardising existing functionality.
A key example of the improvements is how changes to the SearchBar filter are handled. Whereas previously the OverviewView had to check and react to each value, the SearchBar now handles this task independently. This is already evident in the ValueChange listeners of the individual input fields:
codePart.addValueChangeListener(_ -> holdingComponent.safeRefresh());
urlPart.addValueChangeListener(_ -> holdingComponent.safeRefresh());
activeState.addValueChangeListener(_ -> {
holdingComponent.setCurrentPage(1);
holdingComponent.safeRefresh();
});
pageSize.addValueChangeListener(e -> {
holdingComponent.setCurrentPage(1);
holdingComponent.setGridPageSize(e.getValue());
holdingComponent.safeRefresh();
});
These listeners make it clear that the SearchBar takes complete control of the filters’ behaviour and automatically updates the view. Previously, this logic was distributed in the OverviewView. Global search has also been significantly improved. It used to be a standalone input field with no real integration, but now it dynamically controls the specific search fields and ensures consistent filters:
globalSearch.addValueChangeListener(e -> {
var v = Optional.ofNullable(e.getValue()).orElse("");
if (searchScope.getValue().equals("Shortcode")) {
codePart.setValue(v);
urlPart.clear();
} else {
urlPart.setValue(v);
codePart.clear();
}
});
As a result, global search has become a real entry point for filter logic, rather than being an additional field with no straightforward integration.
Another big step forward is the introduction of the reset mechanism, which resets all filters in a targeted manner while ensuring that the UI returns to a consistent state:
resetBtn.addClickListener(_ -> {
try (var _ = withRefreshGuard(true)) {
resetElements();
holdingComponent.setCurrentPage(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
Finally, switching between simple and advanced search also shows the new structural quality of the SearchBar:
advanced.addOpenedChangeListener(ev -> {
boolean nowClosed = !ev.isOpened();
if (nowClosed) {
applyAdvancedToSimpleAndReset();
} else {
setSimpleSearchEnabled(false);
}
});
Overall, the new SearchBar is far superior in both functionality and architecture. Their listeners, control logic, and consistent handling of filter elements form the basis for a modern, scalable, and maintenance-friendly search architecture. It is not only functionally better, but also forms the basis for a modern, scalable and maintenance-friendly search architecture.
Interaction of the SearchBar with grid and backend
The SearchBar only shows its full strength when interacting with the grid and the backend that powers it. While it serves as a central input component for all search and filter parameters on the interface, it technically forms the link between user interaction and server-side data supply. It is precisely in this role that it becomes clear how crucial the decoupling of the filter logic from the actual view is for performance, maintainability and extensibility.
In the first step, the SearchBar accepts all user input – from global search texts to specific shortcode or URL parts to date ranges, sort fields and the active status. These values are no longer processed ad hoc in the OverviewView, but collected, validated and harmonised in a structured form within the SearchBar. This keeps filter states consistent and traceable, even when multiple input fields are changed simultaneously.
The second step is the interaction with the grid. As soon as a relevant filter value changes, the SearchBar notifies the OverviewView of the updated state. This, in turn, triggers a grid update without requiring the individual filter criteria to be known or processed. The grid then calls the backend via its DataProvider and receives a filtered subset of the data based on the SearchBar’s requirements. This creates a clearly separated yet closely interlinked system of input, data retrieval and presentation.
On the backend, the advantage of a structured filter is particularly evident in how the OverviewView configures its DataProvider. The SearchBar always provides a consistent filter object that is passed directly to the backend calls. Central to this is the initialisation of the DataProvider in the OverviewView:
private void initDataProvider() {
dataProvider = new CallbackDataProvider<>(
q -> {
final int uiSize = Optional.ofNullable(searchBar.getPageSize()).orElse(25);
final int pageStart = (currentPage - 1) * uiSize;
final int vLimit = q.getLimit();
final int vOffset = q.getOffset();
final int effectiveLimit = (vLimit > 0) ? vLimit: uiSize;
final int effectiveOffset = pageStart + vOffset;
final int page = (effectiveLimit > 0) ? (effectiveOffset / effectiveLimit) + 1 : 1;
final int size = (effectiveLimit > 0) ? effectiveLimit: uiSize;
final UrlMappingListRequest req = searchBar.buildFilter(page, size);
try {
final List<ShortUrlMapping> items = urlShortenerClient.list(req);
return items.stream();
} catch (IOException ex) {
logger().error("Error fetching (page={}, size={})", page, size, ex);
Notifications.loadingFailed();
return Stream.empty();
}
},
_ -> {
try {
final UrlMappingListRequest base = searchBar.buildFilter(null, null);
totalCount = urlShortenerClient.listCount(base);
final int uiSize = Optional.ofNullable(searchBar.getPageSize()).orElse(25);
final int pageStart = (currentPage - 1) * uiSize;
final int remaining = Math.max(0, totalCount - pageStart);
final int pageCount = Math.min(uiSize, remaining);
refreshPageInfo();
return pageCount;
} catch (IOException ex) {
logger().error("Error counting", ex);
totalCount = 0;
refreshPageInfo();
return 0;
}
}
);
grid.setPageSize(Optional.ofNullable(searchBar.getPageSize()).orElse(25));
grid.setDataProvider(dataProvider);
}
This makes it clear how closely the grid, SearchBar, and backend interact: the DataProvider calculates the effective query parameters from the current paging information and page size, then builds a UrlMappingListRequest from the SearchBar and passes it directly to the URLShortenerClient. The SearchBar is also used for the counting function, this time without paging parameters, to determine the total number of entries.
The advantage of this structure is that the OverviewView itself does not need to know the details of the filter fields. It delegates the entire filter configuration to the SearchBar and focuses solely on controlling the grid and paging. Changes to the filters – such as additional criteria or changed default values – can be made entirely in the SearchBar without having to adjust the DataProvider or the View.
Overall, the interaction among SearchBar, Grid, and the backend shows a cleanly orchestrated data-flow model: Users change a filter, the SearchBar generates a unique search query, the DataProvider requests the appropriate data via the URLShortenerClient, and the Grid presents the results. This consistent, clearly structured process makes the entire interface much more stable, understandable and responsive.
Cheers Sven
Discover more from Sven Ruppert
Subscribe to get the latest posts sent to your email.