Adventskalender 2025 – Komplexe Suche

Sven Ruppert

Die kontinuierliche Weiterentwicklung des URL-Shorteners konzentriert sich seit Beginn auf zwei Kernziele: eine robuste technische Basis ohne externe Frameworks und eine moderne, produktive Benutzeroberfläche, die sowohl intuitiv als auch für Power-User effizient bedienbar ist. Im Rahmen der aktuellen Entwicklungsstufe wurde ein wesentliches UI-Modul überarbeitet – die OverviewView, also die Ansicht, in der Benutzer sämtliche gespeicherten Verkürzungen durchsuchen, filtern und verwalten.

In der vorherigen Version zeigte sich immer deutlicher, dass verschiedene Komponenten in der Suche und der Filterung voneinander entkoppelt waren. Die einfache Suche bot nur begrenzte Funktionalität, während die erweiterten Filter zwar vorhanden waren, jedoch nicht in einen echten Bedienfluss integriert waren. Darüber hinaus war das Zusammenspiel zwischen Benutzerinteraktionen wie dem Reset, der Änderung der Filter, dem Paging oder dem Öffnen der Detailansicht nicht ausreichend stabilisiert, was teilweise zu mehrfachen Refresh-Vorgängen oder widersprüchlichen UI-Zuständen führte.

Mit der nun vorgenommenen Überarbeitung wurden mehrere strukturelle Ziele verfolgt. Zentral stand die Vereinheitlichung der Interaktionen: Die Suchfunktion wurde neu konzipiert und zu einem schlüssigen Gesamtkonzept zusammengeführt, das eine globale Suchleiste mit klar definiertem Scope sowie einen strukturierten Bereich für erweiterte Filter umfasst. Parallel dazu wurde die technische Architektur der Refresh-Mechanismen überarbeitet, um eine ruhigere und zuverlässigere Benutzererfahrung zu schaffen.

Diese Einführung zeichnet zunächst die Motivation hinter den Änderungen nach und ordnet sie in den Gesamtkontext der Entwicklung ein. In den folgenden Kapiteln wird darauf aufbauend zunächst die neue globale Suche beschrieben, bevor die Synchronisationsmechanismen, die erweiterten Filter, die interne Refresh-Architektur sowie die Verbesserungen im Grid im Detail erläutert werden. Das Ziel ist nicht nur eine funktionale Beschreibung, sondern auch eine technische Durchdringung der Mechanismen, die diese Verbesserungen ermöglichen und die Grundlage für kommende Erweiterungen bilden.

Die Quelltexte zu diesem Entwicklungsschritt befinden sich auf GitHub und sind hier zu finden: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-07

Die neue globale Suche

Die Einführung der globalen Suche markiert einen zentralen Schritt hin zu einer konsistenten und zugleich flexiblen Bedienlogik innerhalb der OverviewView. Während in der bisherigen Version die Suchfunktion aus mehreren voneinander unabhängigen Elementen bestand, wurde nun eine einheitliche Interaktionsfläche geschaffen, deren Verhalten klar definiert und technisch sauber modelliert ist. Grundlage bildet ein einzelnes Textfeld, ergänzt um eine Auswahl des Suchbereichs, wodurch die bisherige Trennung zwischen URL- und Shortcode-Suche aufgehoben wird.

Im Quelltext zeigt sich diese Vereinheitlichung zunächst in der expliziten Einführung zweier zentraler UI-Komponenten, die als feste Bestandteile der View deklariert werden:

private final TextField globalSearch = new TextField();
private final ComboBox<String> searchScope = new ComboBox<>("Search in");
//  private final Button advancedBtn = new Button("Advanced filters", new Icon(VaadinIcon.SLIDERS));

Damit ist klar definiert, dass es genau ein globales Suchfeld und genau eine Auswahl für den Suchbereich gibt. Beide Komponenten werden früh im Lebenszyklus der View erzeugt und stehen damit sämtlichen nachfolgenden Konfigurationsschritten zur Verfügung. Die eigentliche Ausgestaltung erfolgt in der Methode buildSearchBar(), in der Platzhalter, Breite und Interaktionsverhalten konkret festgelegt werden:

private Component buildSearchBar() {
    globalSearch.setPlaceholder("Search all…");
    globalSearch.setClearButtonVisible(true);
    globalSearch.setWidth("28rem");
    globalSearch.setValueChangeMode(LAZY);
    globalSearch.setValueChangeTimeout(VALUE_CHANGE_TIMEOUT);

    searchScope.setItems("URL", "Shortcode");
    searchScope.setValue("URL");
    searchScope.setWidth("11rem");

    pageSize.setMin(1);
    pageSize.setMax(500);
    pageSize.setStepButtonsVisible(true);
    pageSize.setWidth("140px");
    // ...
}

Die globale Suche wird hier nicht als beliebiges Textfeld behandelt, sondern bewusst als zentrales Steuerelement modelliert. Der Platzhalter „Search all…“ macht deutlich, dass der Benutzer, unabhängig vom konkreten Suchbereich, zunächst nur einen Suchbegriff eingibt. Das eigentliche Routing dieses Wertes in das passende technische Feld übernimmt die Logik, die an das Suchfeld und die Scope-Auswahl gebunden ist. Die Wahl des LAZY-Modus in Kombination mit einem expliziten Timeout sorgt dafür, dass nicht bei jedem einzelnen Tastendruck serverseitige Filteranfragen ausgelöst werden, sondern erst dann, wenn der Benutzer die Eingabe abgeschlossen hat.

Das Suchfeld dient als Ausgangspunkt für sämtliche Abfragen. Sobald der Benutzer eine Eingabe tätigt, wird der Inhalt in Abhängigkeit vom aktuell gewählten Suchbereich entweder der URL- oder der Shortcode-Komponente des Filtermodells zugeordnet. Diese Zuordnung ist nicht nur ein UI‑seitiger Mechanismus, sondern eine klare Regel innerhalb der internen Logik: Der Wert des globalen Suchfeldes ist stets mit genau einem der beiden Felder des Anforderungsobjekts verknüpft. Die zentrale Umsetzung dieser Kopplung erfolgt über den ValueChangeListener des globalen Suchfeldes:

      var v = Optional.ofNullable(e.getValue()).orElse("");
      if (searchScope.getValue().equals("Shortcode")) {
        codePart.setValue(v);
        urlPart.clear();
      } else {
        urlPart.setValue(v);
        codePart.clear();
      }
    });

Hier wird jeder neue Wert zunächst in eine sichere, nicht‑null‑Variante überführt. Anschließend entscheidet der Wert der ComboBox searchScope, ob der Suchbegriff als Shortcode-Filter (codePart) oder als URL-Filter (urlPart) interpretiert wird. Wichtig ist, dass immer nur eines der beiden Felder belegt wird, während das andere gezielt geleert wird. Auf diese Weise wird vermieden, dass unklare Suchsituationen entstehen, in denen mehrere Filter gleichzeitig unkoordiniert gesetzt werden. Das globale Suchfeld wird damit zur alleinigen Quelle für genau einen bestimmten logischen Filterzustand.

Eine weitere wesentliche Neuerung ist die enge Kopplung zwischen dem Suchfeld und dem Suchbereich. Die Auswahl des Bereichs – URL oder Shortcode – bestimmt direkt, welcher Teil des Filtermodells aktiv ist. Damit diese Beziehung in beide Richtungen konsistent bleibt, reagiert auch die Scope-Auswahl auf Änderungen und spiegelt den aktuellen Suchwert in das jeweils passende Feld:

searchScope.addValueChangeListener(_ -> {
      var v = Optional.ofNullable(globalSearch.getValue()).orElse("");
      if ("Shortcode".equals(searchScope.getValue())) {
        codePart.setValue(v);
        urlPart.clear();
      } else {
        urlPart.setValue(v);
        codePart.clear();
      }
    });

Während der Listener des Textfeldes reagiert, wenn sich der eigentliche Suchbegriff ändert, ist dieser Listener dafür zuständig, den Suchzustand konsistent zu halten, wenn der Benutzer nachträglich den Suchbereich umschaltet. Beide Implementierungen folgen demselben Muster: Ein gemeinsamer Quellwert, der entweder als Shortcode oder als URL interpretiert wird, wobei das jeweils nicht aktive Feld konsequent geleert wird. Dadurch bleibt die Suchoberfläche nicht nur visuell, sondern auch logisch eindeutig.

Das Verhalten der globalen Suche ist darüber hinaus so gestaltet, dass es im Zusammenspiel mit den erweiterten Filtern eine klare Priorisierung ermöglicht. Solange die erweiterten Filter geschlossen sind, kontrolliert das globale Suchfeld den Filterzustand unabhängig davon. Öffnet der Benutzer den erweiterten Bereich, verliert die globale Suche ihre aktive Rolle und wird sowohl visuell als auch technisch in den Hintergrund gestellt. Diese Trennung wird durch eine kleine Hilfsmethode gekapselt, die explizit festlegt, in welchem Zustand sich die einfache Suche befinden darf:

private void setSimpleSearchEnabled(boolean enabled) {
    globalSearch.setEnabled(enabled);
    searchScope.setEnabled(enabled);
    resetBtn.setEnabled(true);
    globalSearch.setHelperText(enabled ? null : "Disabled while Advanced filters are open");
}

Über diese Methode wird nicht nur das Aktivieren und Deaktivieren der Eingabeelemente gesteuert, sondern auch ein kontextsensitiver Hilfetext gesetzt, der den Benutzer darauf hinweist, warum die globale Suche im geöffneten Advanced‑Modus nicht zur Verfügung steht. Auf diese Weise wird verhindert, dass zwei parallele Filterquellen miteinander konkurrieren und den Gesamtzustand destabilisieren. Zugleich bleibt das Bedienkonzept transparent nachvollziehbar, da die UI ihren Status aktiv kommuniziert.

Mit dieser überarbeiteten globalen Suche wurde ein intuitiver, klarer Einstiegspunkt geschaffen, der sowohl aus Sicht des Benutzers als auch aus Sicht der technischen Architektur eine eindeutig definierte Funktion hat. Das Suchfeld, die Scope-Auswahl und die jeweils verknüpften Filterfelder bilden eine kleine, in sich geschlossene Zustandsmaschine, deren Verhalten explizit im Code modelliert ist. Die folgenden Kapitel untersuchen nun, wie diese Suche mit den übrigen Komponenten zusammenwirkt und wie die zugrunde liegende Synchronisationslogik konsistente Zustandsübergänge gewährleistet.

Such-Scopes und Synchronisationslogik

Während die globale Suche dem Benutzer eine klare Einstiegskomponente bietet, entfaltet sich ihre eigentliche Stärke erst im Zusammenspiel mit der dahinterliegenden Synchronisationslogik. Entscheidend ist, dass der gewählte Such-Scope – also die Entscheidung zwischen URL und Shortcode – nicht nur ein visuelles Oberflächen-Detail bleibt, sondern konsequent in das interne Zustandsmodell übertragen wird. Ziel dieser Schicht ist es, sicherzustellen, dass zu jedem Zeitpunkt eindeutig bestimmt werden kann, welcher Filter aktiv ist und welche Teile der UI diesen Filter repräsentieren.

Aus Benutzersicht lässt sich das Verhalten in zwei zentrale Szenarien unterteilen. Im einfachen Modus steuert die Kombination aus globalem Suchfeld und Scope-Auswahl direkt den Filterzustand. Der Benutzer entscheidet implizit über den Eingabekontext, ob er nach Teilen einer Ziel-URL oder nach einem Shortcode suchen möchte. Im erweiterten Modus hingegen wird die globale Suche bewusst zurückgenommen, und die Detailfelder übernehmen vollständig die Kontrolle über den Filterzustand. Dieser Übergang zwischen den Modi ist der Kern der Synchronisationslogik.

Technisch betrachtet basiert diese Logik auf wenigen, aber klar abgegrenzten Prinzipien. Erstens existiert zu jedem Zeitpunkt genau eine Quelle für den effektiven Suchstring. Im einfachen Modus ist dies das globale Suchfeld, das je nach Scope entweder der URL- oder der Shortcode-Komponente des Filtermodells zugeordnet wird. Im erweiterten Modus sind es die dedizierten Felder für URL und Shortcode im Advanced-Bereich, deren Werte direkt in das Anforderungsobjekt übertragen werden. Zweitens darf es keine konkurrierenden Zustände geben: Wenn der Benutzer im Advanced-Modus arbeitet, werden die globalen Suchelemente deaktiviert; wenn er in den einfachen Modus zurückkehrt, wird ein konsistenter Zustand aus den bisherigen Detailwerten abgeleitet.

Die technische Umsetzung dieser Umschaltung beginnt dort, wo die View den Advanced-Bereich als steuerndes Element etabliert. Zentral ist der Listener, der auf Öffnen und Schließen des Details-Containers reagiert:

advanced.addOpenedChangeListener(ev -> {
  boolean nowClosed = !ev.isOpened();
  if (nowClosed) {
    applyAdvancedToSimpleAndReset();
  } else {
    setSimpleSearchEnabled(false);
  }
});

Diese wenigen Zeilen modellieren den gesamten Zustandswechsel zwischen den Modi. Im Moment, in dem der Benutzer den Advanced-Bereich öffnet, wird die einfache Suche gezielt deaktiviert. Beim Schließen wird nicht nur der Advanced-Bereich zugeklappt, sondern über applyAdvancedToSimpleAndReset() gleichzeitig ein Konsolidierungsschritt ausgelöst, der die bisherige Detailkonfiguration in einen einfachen, globalen Suchzustand überführt.

Damit das Deaktivieren der einfachen Suche nicht zu einem inkonsistenten UI-Eindruck führt, kapselt die View die dafür benötigten Anpassungen in einer kleinen Hilfsmethode:

private void setSimpleSearchEnabled(boolean enabled) {
  globalSearch.setEnabled(enabled);
  searchScope.setEnabled(enabled);
  resetBtn.setEnabled(true);
  globalSearch.setHelperText(enabled ? null : "Disabled while Advanced filters are open");
}

Die Methode steuert sowohl die Interaktivität von Suchfeld und Scope-Auswahl als auch den begleitenden Hilfetext. Sobald der Advanced-Bereich geöffnet ist, bleiben die Werte der einfachen Suche zwar erhalten, können jedoch nicht mehr verändert werden. Gleichzeitig macht der Helper-Text transparent, dass die globale Suche derzeit bewusst deaktiviert ist. Auf dieser Ebene wird der erste Teil des oben beschriebenen Prinzips umgesetzt: Es gibt immer nur eine aktive Quelle, die den wirksamen Filterzustand bestimmt.

Die umgekehrte Richtung – vom erweiterten Modus zurück in den einfachen – ist komplexer, weil hier eine Auswahl getroffen werden muss. Im Advanced-Bereich können gleichzeitig ein Shortcode-Fragment und ein URL-Fragment eingetragen sein. Beide wären grundsätzlich als Filter geeignet, lassen sich jedoch nicht ohne Weiteres in ein einziges globales Suchfeld überführen. Genau an dieser Stelle setzt applyAdvancedToSimpleAndReset() an:

private void applyAdvancedToSimpleAndReset() {
  String code = Optional.ofNullable(codePart.getValue()).orElse("").trim();
  String url = Optional.ofNullable(urlPart.getValue()).orElse("").trim();

  final boolean hasCode = !code.isBlank();
  final boolean hasUrl = !url.isBlank();
  final String winnerValue = hasCode ? code : (hasUrl ? url : "");
  final String winnerScope = hasCode ? "Shortcode" : "URL";

  try (var _ = new RefreshGuard(true)) {
    codePart.clear();
    codeCase.clear();
    urlPart.clear();
    urlCase.clear();
    fromDate.clear();
    fromTime.clear();
    toDate.clear();
    toTime.clear();

    sortBy.clear();
    dir.clear();
    sortBy.setValue("createdAt");
    dir.setValue("desc");

    searchScope.setValue(winnerScope);
    if (!winnerValue.isBlank()) {
      globalSearch.setValue(winnerValue);
    } else {
      globalSearch.clear();
    }

    setSimpleSearchEnabled(true);
    globalSearch.focus();
  }
}

Die Methode beginnt mit der Auswertung der Detailfelder codePart und urlPart. Beide Werte werden defensiv in Strings überführt und anschließend auf inhaltliche Nichtleere geprüft. Daraus werden zwei Dinge abgeleitet: ein „Winner“-Wert und ein „Winner“-Scope. Ist ein Shortcode-Fragment gesetzt, erhält dieses Vorrang vor einem etwaigen URL-Fragment. Nur wenn kein Shortcode, sondern eine URL vorhanden ist, wird die URL als Gewinner betrachtet. Ist beides leer, wird mit einem leeren Suchstring gearbeitet und der Scope auf „URL“ zurückgesetzt. Auf diese Weise wird die im Fließtext beschriebene Priorisierung technisch konkret umgesetzt, ohne dass es zu mehrdeutigen Situationen kommen kann.

Im zweiten Block der Methode werden sämtliche Advanced-Felder konsequent zurückgesetzt. Neben den Textfeldern für Shortcode und URL betrifft dies auch die Case-Sensitivity-Checkboxen sowie die Felder für Zeitfenster und Sortierung. Der Advanced-Bereich wird damit in einen definierten Ausgangszustand zurückgeführt. Dank des RefreshGuard findet dieser Reset nicht in Form mehrerer einzelner Refreshes statt, sondern wird als aggregierter Zustandswechsel behandelt, der in einem kontrollierten Reload endet.

Erst danach wird der zuvor ermittelte Gewinnerzustand in die einfache Suche zurückgespiegelt. Der Scope der globalen Suche wird auf winnerScope gesetzt; der globale Suchstring wird entweder mit winnerValue gefüllt oder explizit geleert. Abschließend wird die einfache Suche wieder aktiviert und der Fokus auf das globale Suchfeld gesetzt. Damit erhält der Benutzer nach dem Schließen des Advanced-Bereichs eine klare, reduzierte Oberfläche, die genau einen aktiven Filterzustand repräsentiert: denjenigen, der aus den zuvor gewählten Detailwerten abgeleitet wurde.

In der Summe entsteht dadurch eine kleine, aber präzise Zustandsmaschine. Das Öffnen des Advanced-Bereichs verschiebt die Kontrolle vollständig auf die Detailfelder und deaktiviert die globale Suche sichtbar. Das Schließen löst eine kontrollierte Reduktion auf einen einzelnen, einfach verständlichen Filterzustand aus. Die Synchronisationslogik bleibt dabei vollständig im Code nachvollziehbar, ist über wenige, aber klar strukturierte Methoden gekapselt und lässt sich bei Bedarf um zusätzliche Filterfelder erweitern, ohne das Grundprinzip zu verletzen. Auf dieser Grundlage können die folgenden Kapitel nun die übrigen Aspekte der Filterung, etwa die erweiterten Filterfelder und die Refresh-Architektur, im Detail beleuchten.

Advanced Filters: Konzeption und UI-Design

Die globale Suche bildet den kompakten Einstiegspunkt in die Filterlogik der OverviewView. Ihr Funktionsumfang bleibt jedoch bewusst begrenzt, um eine niedrige Einstiegshürde und eine schnelle Bedienbarkeit zu gewährleisten. Sobald die Anforderungen über ein einfaches Textfragment hinausgehen, stößt dieses Modell an natürliche Grenzen. An dieser Stelle kommen die Advanced Filters ins Spiel, die dem Benutzer eine deutlich feinere Kontrolle über die Filterung von Kurz-URLs eröffnen und gleichzeitig eine strukturierte, visuell nachvollziehbare Oberfläche bereitstellen.

Konzeptionell wurde der Advanced-Bereich als bewusst abgetrennter Modus gestaltet. Er ist nicht als bloße Erweiterung des bestehenden Suchfelds zu verstehen, sondern als eigenständiger Filterkontext, der nur dann aktiv wird, wenn der Benutzer ihn explizit öffnet. Diese Entscheidung trägt zwei Überlegungen Rechnung. Einerseits soll der einfache Modus nicht mit zusätzlichen Optionen überladen werden, die in vielen Alltagsszenarien nicht benötigt werden. Andererseits sollen erweiterte Filteroperationen – etwa die Kombination aus Shortcode-Fragment, URL-Teilstring, Zeitfenster und Sortierung – einen klar identifizierbaren Arbeitsbereich haben, in dem alle zugehörigen Eingabeelemente räumlich gebündelt sind.

Im Quelltext beginnt diese Konzeption bereits auf Feldebene, wo die Komponenten für den Advanced-Bereich klar von der globalen Suche getrennt deklariert werden:

private final TextField codePart = new TextField("Shortcode contains");
private final Checkbox codeCase = new Checkbox("Case-sensitive");
private final TextField urlPart = new TextField("Original URL contains");
private final Checkbox urlCase = new Checkbox("Case-sensitive");
private final DatePicker fromDate = new DatePicker("From (local)");
private final TimePicker fromTime = new TimePicker("Time");
private final DatePicker toDate = new DatePicker("To (local)");
private final TimePicker toTime = new TimePicker("Time");
private final ComboBox<String> sortBy = new ComboBox<>("Sort by");
private final ComboBox<String> dir = new ComboBox<>("Direction");

Diese Felder definieren die semantischen Dimensionen der erweiterten Filter: textuelle Filterung über Shortcode und Original-URL, optional fallweise, ein explizites Zeitfenster sowie Sortierkriterien. Dass sie als eigene Attribute der View geführt werden, ist Ausdruck des oben beschriebenen separaten Modus: Sie gehören nicht zur globalen Suche, sondern zu einer eigenen, erweiterten Sicht auf den Datenraum.

Aus UI-Sicht manifestiert sich diese Trennung in der Verwendung eines Details-Containers, der die erweiterten Filter ein- und ausklappbar macht. Im geschlossenen Zustand nimmt der Advanced-Bereich keinen zusätzlichen Platz ein und signalisiert lediglich durch seinen Header, dass weitere Optionen verfügbar sind. Erst beim Öffnen entfaltet sich ein strukturiertes Formular, das die verschiedenen Filterdimensionen in logisch zusammengehörige Gruppen ordnet. Die konkrete Ausgestaltung beginnt mit der Konfiguration der Felder selbst:

codePart.setPlaceholder("e.g. ex-");
codePart.setValueChangeMode(LAZY);
codePart.setValueChangeTimeout(VALUE_CHANGE_TIMEOUT);
codePart.addValueChangeListener(_ -> safeRefresh());

urlPart.setPlaceholder("e.g. docs");
urlPart.setValueChangeMode(LAZY);
urlPart.setValueChangeTimeout(VALUE_CHANGE_TIMEOUT);
urlPart.addValueChangeListener(_ -> safeRefresh());

sortBy.setItems("createdAt", "shortCode", "originalUrl", "expiresAt");
dir.setItems("asc", "desc");

fromDate.setClearButtonVisible(true);
toDate.setClearButtonVisible(true);
fromTime.setStep(Duration.ofMinutes(15));
toTime.setStep(Duration.ofMinutes(15));
fromTime.setPlaceholder("hh:mm");
toTime.setPlaceholder("hh:mm");

Die Textfelder für Shortcode und URL erhalten aussagekräftige Platzhalter und verwenden, ebenso wie die globale Suche, einen lazy ValueChange-Modus mit Timeout. Dadurch wird verhindert, dass bei jeder Eingabe sofort ein neuer Filterlauf ausgelöst wird, gleichzeitig reagieren die Filter aber ausreichend schnell auf Änderungen. Das Sortierpaar sortBy wird dir mit den zulässigen Werten vorbelegt und so in einen definierten Raum möglicher Sortierstrategien eingebettet. Die Datums- und Zeitfelder werden um Komfortfunktionen ergänzt: Clear-Buttons, feste Zeitraster von 15 Minuten und Platzhalter für das Zeitformat unterstützen den Benutzer bei der Eingabe und reduzieren gleichzeitig die Wahrscheinlichkeit ungültiger Werte.

Die räumliche Struktur der Advanced Filters zielt darauf ab, zusammengehörige Informationen sichtbar zu gruppieren. Statt alle Komponenten in einer langen Zeile zu platzieren, arbeitet die Implementierung mit mehreren vorschalteten Layouts. Zunächst werden die Datums- und Zeitfelder in zwei Gruppen zusammengeführt:

var fromGroup = new HorizontalLayout(fromDate, fromTime);
fromGroup.setDefaultVerticalComponentAlignment(Alignment.END);
var toGroup = new HorizontalLayout(toDate, toTime);
toGroup.setDefaultVerticalComponentAlignment(Alignment.END);

Die beiden HorizontalLayouts fromGroup und toGroup sorgen dafür, dass Datum und Zeit visuell als zusammengehörige Einheit erscheinen. Durch die vertikale Ausrichtung am unteren Rand entsteht ein ruhiges, einheitliches Bild, selbst wenn die Feldhöhen leicht variieren. Diese Gruppen werden anschließend in ein FormLayout eingebettet, das die eigentliche responsiv reagierende Struktur bildet:

FormLayout searchBlock = new FormLayout();
searchBlock.setWidthFull();
searchBlock.add(codePart, urlPart, new HorizontalLayout(codeCase, urlCase));
searchBlock.add(fromGroup, toGroup);
searchBlock.setResponsiveSteps(
    new FormLayout.ResponsiveStep("0", 1),
    new FormLayout.ResponsiveStep("32rem", 2),
    new FormLayout.ResponsiveStep("56rem", 3)
);

Hier werden zunächst die textuellen Filter – Shortcode und URL – sowie die zugehörigen Case-Sensitivity-Checkboxen gemeinsam in einem Block angeordnet. Darunter folgen die Gruppen für das Zeitfenster. Über ResponsiveSteps wird festgelegt, wie viele Spalten das Layout bei unterschiedlichen Breiten verwenden darf. Unterhalb von 32 rem wird eine einspaltige Darstellung gewählt, bei mittlerer Breite stehen zwei Spalten zur Verfügung, und ab 56 rem kann das Layout auf drei Spalten erweitert werden. Auf diese Weise bleibt die Eingabemaske sowohl in breiten Desktopansichten als auch in schmaleren Fenstern oder geteilten Bildschirmen lesbar und gut strukturiert.

Die Sortiersteuerung wird bewusst optisch vom Suchblock entkoppelt, aber auf derselben horizontalen Achse positioniert. Dazu wird ein eigener Toolbar-Bereich aufgebaut:

sortBy.setLabel(null);
sortBy.setPlaceholder("Sort by");
sortBy.setWidth("12rem");

dir.setLabel(null);
dir.setPlaceholder("Direction");
dir.setWidth("8rem");

HorizontalLayout sortToolbar = new HorizontalLayout(sortBy, dir);
sortToolbar.setAlignItems(FlexComponent.Alignment.END);

Indem die Labels der ComboBoxen entfernt und stattdessen Platzhalter verwendet werden, bleibt die Oberfläche kompakt, ohne an Verständlichkeit zu verlieren. Die feste Breite sorgt für eine stabile Ausrichtung, während die horizontale Gruppierung deutlich macht, dass beide Felder funktional zusammengehören. Die Ausrichtung am unteren Rand fügt sich in die Gestaltung des restlichen Headers ein, in dem die Filterfelder ebenfalls an einer gemeinsamen Grundlinie ausgerichtet werden.

Schließlich werden Suchblock und Sortierleiste in einem gemeinsamen Header-Layout zusammengeführt, das den sichtbaren Inhalt des Advanced-Bereichs bildet. Dieses Header-Layout wird dann in eine Details-Komponente eingebettet:

HorizontalLayout advHeader = new HorizontalLayout(searchBlock, sortToolbar);
advHeader.setWidthFull();
advHeader.setSpacing(true);
advHeader.setAlignItems(FlexComponent.Alignment.START);
advHeader.expand(searchBlock);
advHeader.getStyle().set("flex-wrap", "wrap");
advHeader.setVerticalComponentAlignment(FlexComponent.Alignment.END, sortToolbar);

advanced = new Details("Advanced filters", advHeader);
advanced.setOpened(false);
advanced.getElement().getThemeList().add("filled");

setSimpleSearchEnabled(!advanced.isOpened());

Der advHeader sorgt dafür, dass der Suchblock den verfügbaren Platz einnimmt, während die Sortierwerkzeuge am rechten Rand verankert werden. Durch das Aktivieren von flex-wrap kann der Header bei begrenzter Breite in mehrere Zeilen umbrechen, ohne dass die logische Nähe der Elemente verloren geht. Die Details-Komponente umfasst diesen Header und macht den gesamten Advanced-Bereich ein- und ausklappbar. Im geschlossenen Zustand bleibt lediglich der Titel „Advanced filters“ sichtbar, im geöffneten Zustand entfaltet sich das vollständige Formular. Die Anwendung des Themes filled verleiht dem Bereich zudem eine optische Abgrenzung gegenüber dem umgebenden Layout.

Inhaltlich folgt die Gestaltung der Advanced Filters dem Prinzip, die wesentlichen Dimensionen der Daten direkt zugänglich zu machen. Shortcodes und Ziel-URLs bilden die textuellen Einstiegspunkte, ergänzt um die Möglichkeit, die Interpretation als case-sensitive oder case-insensitive Suche zu steuern. Der zeitliche Kontext der Kurz-URL – etwa das betrachtete Erstellungs- oder Ablaufintervall – wird über kombinierte Datums- und Zeitfelder abgebildet, die explizit im lokalen Kontext des Benutzers arbeiten. Abschließend erlauben Sortierfeld und Sortierrichtung eine feine Steuerung der Reihenfolge der angezeigten Einträge, sodass sich etwa neu angelegte oder bald ablaufende Links gezielt in den Vordergrund holen lassen.

Wichtig ist, dass dieser erweiterte Funktionsumfang nicht dazu führt, dass der Benutzer mit einer unübersichtlichen Anzahl an Bedienelementen konfrontiert wird. Die klare räumliche Trennung durch den aufklappbaren Container, die durchdachte Gruppierung der Felder und die responsive Anordnung bilden deshalb nicht nur eine ästhetische, sondern vor allem eine kognitive Entlastung. Der Benutzer kann in Ruhe entscheiden, ob er sich mit den Standardmöglichkeiten der globalen Suche begnügt oder in einen Expertenmodus wechselt, der ihm eine detaillierte Kontrolle über die Datenbasis bietet.

Auf dieser Grundlage kann der Advanced-Bereich in den folgenden Kapiteln nahtlos in die restliche Architektur eingebettet werden. Insbesondere die Verbindung zur zuvor beschriebenen Synchronisationslogik und zur Refresh-Architektur zeigt, wie das UI-Design und die interne Zustandsmaschine Hand in Hand arbeiten, um auch komplexere Filteranfragen stabil und gut nachvollziehbar zu halten.

Cheer Sven

Total
0
Shares
Previous Post

Adventskalender 2025 – Einführung multipler Aliasse – Teil 2

Next Post

Adventskalender 2025 – Von UI-Interaktionen zu einer deterministischen Refresh-Architektur

Related Posts