Adventskalender 2025 – Komponenten extrahieren – Teil 1

Sven Ruppert

Der heutige Tag unseres Adventskalenders markiert einen entscheidenden Schritt in der Weiterentwicklung der Benutzeroberfläche des URL-Shorteners. Nachdem in den vergangenen Tagen vor allem funktionale Erweiterungen im Vordergrund standen – von Filter- und Suchfunktionen bis hin zu Bulk-Operationen – widmet sich dieser Tag einem strukturellen Feinschliff: dem Refactoring zentraler UI-Komponenten. Dieses Refactoring dient nicht nur der Aufräumarbeit im Code, sondern schafft auch eine klare, modulare Basis für zukünftige Erweiterungen sowie eine deutlich verbesserte Developer Experience.

Die Quelltexte zu diesem Artikel befinden sich auf GitHub unter: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-11

Hier ein Screenshot vom aktuellen Stand der Entwicklung aus Sicht des Benutzers.

Im Fokus steht die Aufteilung der zuvor monolithischen OverviewView in klar abgegrenzte, wiederverwendbare Bestandteile wie die BulkActionsBar und die SearchBar. Diese Trennung verbessert die Wartbarkeit, erhöht die Übersichtlichkeit und erleichtert das Testen einzelner Funktionsbereiche. Gleichzeitig wird damit der Weg geebnet, um weitere UI-Optimierungen und Interaktionsmuster in den kommenden Tagen schrittweise umzusetzen.

Motivation für das Refactoring

Mit zunehmender Funktionalität einer Anwendung wächst unweigerlich auch die Komplexität des Quellcodes. Besonders in einer UI, die kontinuierlich erweitert und an neue Anforderungen angepasst wird, führt dies schnell zu monolithischen Strukturen, in denen Darstellung, Logik und Zustandsverwaltung eng miteinander verwoben sind. Genau diese Situation hatte sich im Laufe der vergangenen Entwicklungstage in der OverviewView aufgebaut: eine zentrale Ansicht, die immer mehr Aufgaben übernehmen musste und dadurch schwerer zu verstehen, zu erweitern und zu testen wurde.

Das Refactoring heute setzt daher an einem fundamentalen Punkt an. Ziel ist nicht die Einführung neuer Features, sondern die Schaffung eines klaren, modularen Fundaments, das künftige Erweiterungen deutlich erleichtert. Einzelne UI-Bausteine werden aus der überladenen View herausgelöst und zu eigenständigen Komponenten geformt – mit verantwortungsspezifischen Aufgaben, klaren Kommunikationswegen und einer deutlich verbesserten Wiederverwendbarkeit. Dies reduziert nicht nur die Komplexität innerhalb der View selbst, sondern macht die gesamte Architektur robuster gegenüber Änderungen.

Gleichzeitig stärkt dieses Refactoring die Developer Experience: Die Struktur des Codes wird nachvollziehbarer, Komponenten können isoliert verbessert werden, und zukünftige Änderungen – etwa an der Suchlogik oder an den Bulk-Operationen – lassen sich zielgerichtet an der richtigen Stelle vornehmen. Das Ergebnis ist ein UI-Design, das stabiler, flexibler und langfristig besser wartbar ist.

Warum dieser Schritt heute ein Wendepunkt ist

Heute markiert einen wichtigen Moment in der Entwicklung der Benutzeroberfläche des URL-Shorteners, weil sich hier erstmals der Fokus bewusst von neuen Funktionen hin zur strukturellen Qualität des Codes verschiebt. Während die vorangegangenen Schritte vor allem darauf abzielten, dem Anwender sichtbare Verbesserungen zu liefern, geht es nun um die Grundlage, auf der diese Funktionen langfristig solide und erweiterbar bleiben. Ein solches Refactoring schafft Klarheit in Bereichen, die zuvor gewachsen statt geplant waren – und genau das macht diesen Tag zu einem Wendepunkt.

Durch die Herauslösung der zentralen UI-Bausteine entsteht eine Architektur, die nicht mehr aus einer einzigen, schwergewichtigen View besteht, sondern aus klar abgegrenzten Komponenten, die genau definierte Aufgaben übernehmen. Diese strukturelle Neuausrichtung eröffnet neue Möglichkeiten für Erweiterungen, verhindert das Fortschreiten des technischen Schuldenabbaus und sorgt dafür, dass kommende Features nicht mehr in einen unübersichtlichen Block integriert werden müssen. Stattdessen kann jede Funktion an der passenden Stelle implementiert werden, ohne bestehende Bereiche zu destabilisieren.

Damit trägt dieser Schritt entscheidend dazu bei, die Entwicklung langfristig zu beschleunigen: weniger Reibungsverluste im Code, weniger Seiteneffekte, mehr Übersichtlichkeit. Heute geht es also nicht um sichtbare Neuerungen, sondern um die Qualität des Fundaments – und genau dadurch wird die Arbeit an kommenden Erweiterungen deutlich effizienter, stabiler und angenehmer.

Übersicht der wichtigsten Änderungen

Im Mittelpunkt steht heute die Entlastung der bisher zentralen OverviewView, die im Laufe der Entwicklung zunehmend Verantwortung übernommen hatte und entsprechend unübersichtlich geworden war. Durch die Extraktion eigenständiger Komponenten wird diese Komplexität aufgebrochen und in klar definierte Bausteine überführt.

Ein wesentlicher Teil dieser Umstrukturierung ist die Einführung der neuen BulkActionsBar, die sämtliche Massenoperationen bündelt und damit sowohl den Code als auch die Benutzerführung übersichtlicher gestaltet. Ebenso wird mit der neuen SearchBar ein dedizierter Bereich geschaffen, der Such- und Filterfunktionen umfasst und dadurch besser erweiterbar ist. Beide Komponenten tragen nicht nur zu einer deutlichen Vereinfachung der View bei, sondern schaffen auch eine Grundlage für konsistentes Design und wiederkehrende Interaktionsmuster in der gesamten Benutzeroberfläche.

Darüber hinaus wurde die interne Logik der Übersicht gestrafft: Zustandsverwaltung, Event-Handling und Grid-Interaktionen wurden neu geordnet, um unerwünschte Abhängigkeiten zu reduzieren und zukünftige Änderungen gezielt umzusetzen. Auch kleinere Verbesserungen, wie die vereinheitlichte Formatierung bestimmter UI-Komponenten, tragen zum Gesamteindruck eines gereinigten und strukturierten Codes bei.

Warum UI-Komponenten aus Views herausgelöst werden

In gewachsenen Benutzeroberflächen entsteht häufig eine enge Verzahnung von Layout, Interaktionslogik und Zustandsverwaltung. Dies führt dazu, dass zentrale Views mit der Zeit zunehmend umfangreicher werden und ihren ursprünglichen Charakter verlieren. Aus einem übersichtlichen Einstiegspunkt der Anwendung wird ein komplexer Block, dessen interne Logik nur noch schwer nachvollziehbar ist. Genau hier setzt das Herauslösen eigenständiger Komponenten an: Es dient dazu, Verantwortlichkeiten klar zu trennen und die Komplexität dorthin zu verschieben, wo sie hingehört – in kleine, klar umrissene Bausteine.

Durch die Extraktion von UI-Komponenten wird die View selbst wieder auf ihre Kernaufgabe reduziert: Sie orchestriert das Zusammenspiel mehrerer, sauber definierter Elemente. Jede Komponente übernimmt dabei eine klar abgegrenzte Rolle – sei es die Verwaltung von Suchfiltern, das Auslösen von Bulk-Operationen oder die Darstellung einzelner UI-Abschnitte. Dieser modulare Ansatz führt zu besserer Lesbarkeit, einem natürlicheren Verständnis der Architektur und einer deutlichen Verringerung von Seiteneffekten bei Änderungen.

Ein weiterer Vorteil ist die Wiederverwendbarkeit. Komponenten, die nur lose mit der Gesamtsicht gekoppelt sind, können flexibel an anderer Stelle eingesetzt, erweitert oder isoliert verbessert werden. Dadurch entstehen nicht nur eine robuste Struktur, sondern auch eine nachhaltigere Entwicklungspraxis, in der neue Funktionen ohne große Eingriffe in bestehende Bereiche umgesetzt werden können. Das Herauslösen von UI-Komponenten ist deshalb ein wesentlicher Schritt, um ein wachsendes Projekt langfristig stabil und übersichtlich zu halten.

Die neue BulkActionsBar

Bulk-Operationen gehören zu den Funktionen, die in der täglichen Nutzung eines URL-Shorteners eine zunehmend wichtige Rolle spielen. Sobald die Anzahl der Einträge wächst, steigt der Bedarf, mehrere Elemente gleichzeitig zu bearbeiten – etwa um abgelaufene Links zu deaktivieren, veraltete Kampagnen zu löschen oder eine größere Menge neuer Einträge gemeinsam zu verwalten. Solche Vorgänge sind nicht nur ein Komfortmerkmal, sondern auch ein wesentlicher Bestandteil effizienter Arbeitsabläufe.

In vielen Projekten werden Bulk-Operationen zunächst direkt in die Hauptansicht integriert. Dies funktioniert anfangs gut, führt jedoch langfristig zu einer Überlastung der View. Buttons, Zustandsprüfungen und komplexere Interaktionslogiken beginnen, sich zwischen Grid und View zu vermischen. Das Ergebnis ist eine Struktur, die schwer zu warten ist und bei der Erweiterungen stets mit dem Risiko unerwünschter Seiteneffekte verbunden sind.

Die Entscheidung, Bulk-Operationen in eine eigene Komponente auszulagern, schafft hier eine klare Trennung: Die BulkActionsBar übernimmt den gesamten UI-bezogenen Teil der Massenaktionen und bildet damit einen dedizierten Funktionsbereich. Sie bündelt alle relevanten Aktionen an einem Ort, sorgt für ein konsistentes Nutzererlebnis und reduziert zugleich die Komplexität in der übergeordneten View. Durch diese klare Abgrenzung wird es deutlich einfacher, neue Aktionen hinzuzufügen, bestehende zu erweitern oder die Darstellung an zukünftige Designanforderungen anzupassen.

Damit trägt die Auslagerung der Bulk-Operationen entscheidend zu einer stabileren und flexibleren Architektur bei – und stellt sicher, dass die Benutzeroberfläche auch bei wachsenden Anforderungen übersichtlich und intuitiv bleibt.

Aufbau der BulkActionsBar-Komponente

Die BulkActionsBar ist als eigenständige UI-Komponente konzipiert, deren Hauptaufgabe darin besteht, sämtliche Massenaktionen übersichtlich zu bündeln und klar strukturiert bereitzustellen. Ihr Aufbau folgt dem Prinzip, dass eine Komponente genau einen Verantwortungsbereich abdecken soll: In diesem Fall das Initiieren von Sammelaktionen, ohne selbst Logik zur Datenverarbeitung zu enthalten. Dadurch bleibt sie leicht verständlich und flexibel einsetzbar.

Zentraler Bestandteil der BulkActionsBar ist eine klar definierte Sammlung von Interaktionselementen – typischerweise Buttons oder Icons –, die jeweils eine bestimmte Aktion auslösen. Diese Elemente sind kompakt in einem horizontalen Layout angeordnet, sodass sie in der Benutzeroberfläche gut sichtbar und intuitiv erreichbar sind. Ergänzt wird dies durch einen Mechanismus, der die Sichtbarkeit oder Aktivierbarkeit der Aktionen steuert, abhängig davon, ob und wie viele Einträge im Grid ausgewählt wurden. So bleibt die Komponente nicht nur übersichtlich, sondern führt den Benutzer durch klare Interaktionshinweise.

Ein weiterer wesentlicher Aspekt des Aufbaus ist die Abstraktion der Ereignisverarbeitung. Die Komponente selbst führt keine Operationen aus, sondern signalisiert der übergeordneten View über Events oder Callback-Funktionen, welche Aktion ausgelöst werden soll. Dadurch entsteht eine lose Kopplung zwischen UI und Logik, die sowohl das Testen erleichtert als auch Anpassungen an einzelnen Bereichen ermöglicht, ohne andere Komponenten ungewollt zu beeinflussen.

Ein praktisches Beispiel dafür ist der grundlegende Aufbau der Klasse, die die BulkActionsBar definiert:

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();
  }
}

Hier wird gut sichtbar, wie die Komponente alle für die Massenaktionen relevanten Elemente kapselt: Sie kennt den URLShortenerClient, das zugrunde liegende Grid<ShortUrlMapping> und die übergeordnete OverviewView, bleibt aber dennoch als eigenständiger Baustein klar abgegrenzt. Alle Buttons und der Bereich für die Auswahlinformation sind innerhalb der Klasse definiert und werden im Konstruktor initialisiert.

Der eigentliche visuelle Aufbau der BulkActionsBar wird in einer eigenen Methode gekapselt:

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);
}

Die Methode zeigt, wie Stil, Layout und Interaktionselemente an einer zentralen Stelle zusammengeführt werden. Die BulkActionsBar definiert damit ein eigenes visuelles und funktionales Cluster innerhalb der Oberfläche, während die konkrete Ausführung der Aktionen weiterhin über den URLShortenerClient und die OverviewView abgewickelt wird.

Insgesamt ermöglicht dieser Aufbau eine saubere Trennung von Darstellung und Verhalten, erzeugt eine robuste Grundlage für zukünftige Erweiterungen und fügt sich harmonisch in die modulare Architektur der Anwendung ein.

Verwendung der BulkActionsBar in der OverviewView

Die BulkActionsBar entfaltet ihren Nutzen erst im Zusammenspiel mit der OverviewView, denn dort wird sichtbar, wie stark die Auslagerung der Massenaktionen die Struktur der Benutzeroberfläche vereinfacht. Während diese Aktionen zuvor direkt in der View selbst implementiert waren und dadurch eine Vielzahl an Event-Handlern, UI-Elementen und Logik vermischt wurden, übernimmt die BulkActionsBar nun diesen gesamten Funktionsbereich. Die OverviewView muss lediglich definieren, wann die Leiste sichtbar wird, welche Einträge ausgewählt sind und wie auf die ausgelösten Aktionen reagiert werden soll.

Die Integration folgt dabei einem klaren Muster: Sobald der Benutzer eine oder mehrere Zeilen im Grid markiert, blendet die View die BulkActionsBar ein und übergibt ihr den aktuellen Auswahlzustand. Die Komponente selbst führt keine Operationen aus, sondern signalisiert über klar definierte Methoden und Ereignisse, welche Aktion ausgelöst wurde. Dadurch entsteht eine lose Kopplung, die die Übersichtlichkeit erhöht und die View deutlich entlastet.

Ein weiterer Vorteil zeigt sich im Umgang mit Zustandsänderungen. Die OverviewView muss keine Buttons mehr einzeln aktivieren oder deaktivieren oder komplexe UI-Abhängigkeiten steuern. Stattdessen reicht eine zentrale Rückmeldung an die BulkActionsBar, die sich daraufhin selbst aktualisiert. Dieses Zusammenspiel macht den Code nicht nur schlanker, sondern verhindert auch Fehler, die bei mehrfach verstreuter Logik leicht auftreten könnten.

Ein zentraler Einstiegspunkt für die Integration ist die Initialisierung der BulkActionsBar direkt als Feld der OverviewView:

private final BulkActionsBar bulkBar = new BulkActionsBar(urlShortenerClient, grid, this);

Damit erhält die View eine vollständig konfigurierte Instanz, die sowohl den URLShortenerClient als auch das Grid und die View selbst kennt. Eingebunden wird die Komponente anschließend im Konstruktor:

add(bulkBar);
add(grid);

Besonders wichtig ist das Zusammenspiel mit dem Selection-Listener des Grids. Hier entscheidet die View, ob die BulkActionsBar sichtbar ist und welche Informationen sie anzeigen soll:

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);
});

Dieser Abschnitt verdeutlicht die klare Rollenverteilung: Die View erkennt den aktuellen Zustand der Selektion, aktualisiert die Sichtbarkeit der BulkActionsBar und übergibt relevante Statusinformationen. Die Komponente selbst bleibt dabei vollständig frei von Logik zur Auswahlverwaltung.

Auch das Auslösen der eigentlichen Aktionen bleibt in der View gebündelt. Ein Beispiel hierfür ist das Löschen per Tastaturkürzel:

current.addShortcutListener(_ -> {
      if (!grid.getSelectedItems().isEmpty()) {
        bulkBar.confirmBulkDeleteSelected();
      }
    },
    Key.DELETE);

Hier zeigt sich erneut das Zusammenspiel aus klar abgegrenzten Verantwortlichkeiten: Die BulkActionsBar kapselt die UI der Aktion, die OverviewView entscheidet über den Kontext, in dem sie ausgeführt wird.

Insgesamt zeigt die Verwendung der BulkActionsBar in der OverviewView sehr deutlich, wie effektiv eine klare Trennung von Verantwortlichkeiten die Architektur einer Vaadin-Anwendung verbessert. Die View konzentriert sich wieder auf ihre Kernaufgabe – das Präsentieren und Aktualisieren der Daten – während die Komponente die gesamte Interaktion rund um Massenaktionen kapselt und konsistent bereitstellt. der BulkActionsBar in der OverviewView sehr deutlich, wie effektiv eine klare Trennung von Verantwortlichkeiten die Architektur einer Vaadin-Anwendung verbessert. Die View konzentriert sich wieder auf ihre Kernaufgabe – das Präsentieren und Aktualisieren der Daten – während die Komponente die gesamte Interaktion rund um Massenaktionen kapselt und konsistent bereitstellt.

Codevergleich: Vorher vs. Nachher

Der Unterschied zwischen der früheren Implementierung der Bulk-Operationen und der neuen, komponentenbasierten Struktur fällt sofort ins Auge. Während zuvor alle Elemente, Buttons und Dialoge direkt in der OverviewView verankert waren, verteilt über zahlreiche Event-Handler und UI-Bereiche, ist der Code heute deutlich modularer aufgebaut. Die BulkActionsBar fungiert als eigenständiger Baustein, der das gesamte UI für Massenaktionen kapselt. Dadurch musste die OverviewView nicht nur weniger Logik aufnehmen, sondern gewann insgesamt an Übersichtlichkeit und Klarheit.

Besonders sichtbar wird der Unterschied in der Reduktion der Verantwortlichkeiten innerhalb der View. Vor dem Refactoring war die OverviewView für sämtliche Aspekte zuständig: das Vorhalten der Buttons, die Darstellung der Leiste, das Handling der Selektion, das Öffnen und Ausführen der Dialoge sowie die entsprechenden Rückmeldungen an den Benutzer. Dies führte zu einem sowohl umfangreichen als auch eng gekoppelten Codeblock, der schwer zu pflegen und fehleranfällig war.

Nach dem Refactoring hat sich die Struktur deutlich verbessert. Die gesamte UI-bezogene Logik der Bulk-Operationen liegt nun ausschließlich in der BulkActionsBar. Die OverviewView beschränkt sich auf das Erkennen der Selektion und das Weiterleiten der notwendigen Informationen. Das Auslösen der Aktionen erfolgt weiterhin über die Komponente selbst, aber die View ist nicht mehr in den visuellen oder strukturellen Aufbau dieser UI involviert.

Dieser Wandel führt zu einer spürbaren Verbesserung der Lesbarkeit, da jeder Funktionsbereich nun an der Stelle zu finden ist, an der er gehört. Zudem reduziert sich das Risiko unbeabsichtigter Seiteneffekte deutlich, da Änderungen an der BulkActionsBar keine direkten Eingriffe in die OverviewView mehr voraussetzen. Um den Unterschied klar zu machen, lohnt sich ein Blick auf typische Stellen, die früher direkt in der OverviewView lagen und heute ausgelagert sind. Ein klassisches Beispiel ist die Behandlung der Bulk‑Löschoperation. Früher hätte die OverviewView selbst Dialog, Schleife, Fehlerbehandlung und Refresh übernommen. Heute findet sich die gesamte Logik klar abgeschlossen in der BulkActionsBar:

Neue Struktur – ausgelagerte Bulk-Delete-Logik:

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();
}

Hier wird sichtbar, dass die komplette Interaktion – von der UI über die Fehlerbehandlung bis hin zur Aktualisierung – nun innerhalb der Komponente stattfindet und nicht mehr die OverviewView belastet.

Neue Struktur – View signalisiert nur noch die Selektion:

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);
});

Dies zeigt die neue Rollenverteilung: Die OverviewView steuert nur noch die Sichtbarkeit und die Statusanzeige. Früher wäre hier zusätzlich die gesamte Logik der Operation implementiert worden.

Ein weiteres Beispiel ist das Öffnen der Bulk-Set-Expiry-Dialoge. Auch diese Logik liegt nun vollständig in der Komponente und nicht mehr verteilt über die View:

bulkSetExpiryBtn.addClickListener(_ -> openBulkSetExpiryDialog());

Die gesamte Dialoglogik befindet sich anschließend in:

private void openBulkSetExpiryDialog() { ... }

Ergebnis:
Durch die klare Trennung von Verantwortlichkeiten ergeben sich:

  • deutlich weniger Codezeilen in der OverviewView,
  • eine besser strukturierte, modularere Architektur,
  • geringere Kopplung,
  • verbesserte Wartbarkeit und Erweiterbarkeit.

Insgesamt resultiert dies in einer wartungsfreundlicheren, erweiterbaren und klaren strukturierten Architektur.

Anforderungen an die Suche

Eine effektive Suchfunktion ist ein zentraler Bestandteil jeder Verwaltungsoberfläche, insbesondere wenn die Anzahl der Einträge kontinuierlich wächst. Im Kontext von URL-Shorteners bedeutet dies, dass Benutzer schnell und gezielt auf bestimmte Kurzlinks zuzugreifen können – unabhängig davon, ob sie nach einem konkreten Shortcode, einem URL-Fragment, dem Aktivstatus oder nach zeitlichen Kriterien suchen. Die bisherige Implementierung bot lediglich einfache Filtermöglichkeiten und war eng mit der OverviewView verknüpft, was sowohl die Erweiterbarkeit als auch die Wartung erschwerte.

Die Anforderungen an eine moderne Suchkomponente gehen jedoch weit über ein schlichtes Textfeld hinaus. Sie muss unterschiedliche Filterkriterien unterstützen, flexibel auf neue Felder reagieren und sich dynamisch an die Datenstruktur des Backends anpassen. Gleichzeitig soll sie dem Benutzer ein intuitives, konsistentes und nicht überladenes Interface bieten. Dies umfasst klare Eingabebereiche, verständliche Beschriftungen, sinnvolle Standardwerte sowie die Möglichkeit, Suchparameter schnell zu ändern oder zurückzusetzen.

Ebenso wichtig wie die Benutzerfreundlichkeit ist die technische Zuverlässigkeit: Die Suchfunktion darf das Backend nicht unnötig belasten, muss performante Anfragen stellen und sollte serverseitige Filter effizient nutzen. Eine saubere Trennung zwischen UI, Filterlogik und Datenabruf ermöglicht nicht nur eine bessere Übersichtlichkeit, sondern auch spätere Erweiterungen – etwa zusätzliche Filterfelder, Sortieroptionen oder fortgeschrittene Funktionen wie das Kombinieren mehrerer Kriterien.

Mit der Einführung der neuen SearchBar wird diesen Anforderungen Rechnung getragen. Sie bildet das umfassende Filter- und Steuerzentrum für die Übersicht und stellt sicher, dass die View selbst von der Filterlogik entlastet wird. Damit legt sie die Grundlage für ein skalierbares und benutzerfreundliches Such- und Filtererlebnis.

Die SearchBar ist als eigenständige UI-Komponente konzipiert, die sämtliche Filter- und Suchparameter der OverviewView bündelt und in einem klar strukturierten Format bereitstellt. Ziel dieser Komponente ist es, die zuvor verstreuten Filterlogiken zu zentralisieren und die Übersichtlichkeit der View deutlich zu erhöhen. Während zuvor einzelne Eingabefelder und Sortierparameter direkt in der OverviewView definiert wurden, übernimmt nun die SearchBar die gesamte Verantwortung für deren Verwaltung.

Ihr Aufbau folgt einem modularen Konzept: Jede Eingabe – sei es ein Textfilter, ein Sortierkriterium, die Anzahl der angezeigten Elemente oder die Filterung nach Eigenschaften wie Aktivstatus oder Ablaufdatum – ist innerhalb der Komponente klar abgegrenzt und wird eigenständig verarbeitet. Dies sorgt nicht nur für eine bessere logische Trennung, sondern ermöglicht auch eine flexible Erweiterung um zusätzliche Filter, ohne dass die View selbst angepasst werden muss.

Die interne Logik der SearchBar arbeitet eng mit dem Backend zusammen. Aus den vom Benutzer gewählten Parametern erzeugt die Komponente strukturierte Filterobjekte, die konsistent an den Server übergeben werden können. Statt einzelne Parameter lose zu sammeln oder in der View zu kombinieren, werden sie in einem klar definierten Ablauf zusammengeführt: validiert, normalisiert und anschließend in eine Backend-Anfrage überführt.

Wie dieser Ablauf konkret aussieht, zeigt die zentrale Methode buildFilter, die aus den UI-Eingaben ein UrlMappingListRequest-Objekt erzeugt:

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;
}

Der Code zeigt, wie alle relevanten Filterfelder – Active-Status, Shortcode- und URL-Teile, Zeitfenster sowie Sortier- und Paging-Informationen – an einer Stelle zusammengeführt werden. Die SearchBar übernimmt damit die Rolle eines Übersetzers zwischen UI-Eingaben und der im Backend domänenspezifischen Filterstruktur. Auf diese Weise entsteht eine robuste Schnittstelle, die sowohl Fehler reduziert als auch künftige Erweiterungen erleichtert.

Gleichzeitig sorgt die SearchBar für ein konsistentes Nutzererlebnis. Änderungen an einem Feld lösen automatisch die Aktualisierung der Ergebnisliste aus, während sinnvolle Standardwerte und eine einheitliche Darstellung für ein vertrautes Bediengefühl sorgen. Diese Kombination aus struktureller Klarheit, technischer Präzision und Benutzerfreundlichkeit macht die SearchBar zu einem zentralen Baustein der modernen UI-Architektur des Projekts.

Verbesserungen gegenüber der bisherigen Lösung

Technisch betrachtet bietet die neue Lösung eine höhere Robustheit. Die Verarbeitung der Filter erfolgt jetzt vollständig in der SearchBar und nutzt strukturierte Datenobjekte, die präzise an das Backend übermittelt werden. Dies minimiert Fehler, die durch uneinheitliche oder unvollständige Filterparameter entstehen können. Gleichzeitig ermöglicht die klare Struktur das einfache Hinzufügen neuer Filter, ohne die bestehende Funktionalität zu gefährden.

Ein zentrales Beispiel für die Verbesserungen findet sich in der Art und Weise, wie Änderungen an der SearchBar-Filter verarbeitet werden. Während zuvor die OverviewView selbst jeden Wert prüfen und darauf reagieren musste, übernimmt die SearchBar diese Aufgabe nun vollständig eigenständig. Dies zeigt sich bereits in den ValueChange-Listenern der einzelnen Eingabefelder:

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();
});

Diese Listener machen deutlich: Die SearchBar übernimmt vollständig die Kontrolle über das Verhalten der Filter und sorgt selbstständig dafür, dass die View aktualisiert wird. Früher war diese Logik in der OverviewView verteilt.

Auch die globale Suche wurde deutlich verbessert. Früher war sie ein eigenständiges Eingabefeld ohne echte Integration, nun steuert sie dynamisch die spezifischen Suchfelder und sorgt für konsistente Filter:

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();
  }
});

Dadurch ist die globale Suche zu einem echten Einstiegspunkt für die Filterlogik geworden, statt ein zusätzliches Feld ohne klare Integration zu sein.

Ein weiterer großer Fortschritt liegt in der Einführung des Reset-Mechanismus, der alle Filter gezielt zurücksetzt und gleichzeitig sicherstellt, dass die UI in einen konsistenten Zustand zurückkehrt:

resetBtn.addClickListener(_ -> {
  try (var _ = withRefreshGuard(true)) {
    resetElements();
    holdingComponent.setCurrentPage(1);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
});

Schließlich zeigt auch das Umschalten zwischen einfacher und erweiterter Suche die neue strukturelle Qualität der SearchBar:

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

Insgesamt zeigt sich, dass die neue SearchBar sowohl funktional als auch architektonisch weit überlegen ist. Ihre Listener, Steuerlogik und ihr konsistenter Umgang mit den Filterelementen bilden die Grundlage für eine moderne, skalierbare und wartungsfreundliche Sucharchitektur. nicht nur funktional besser ist, sondern auch die Grundlage für eine moderne, skalierbare und wartungsfreundliche Sucharchitektur bildet.

Zusammenspiel der SearchBar mit Grid und Backend

Die SearchBar entfaltet ihre volle Stärke erst im Zusammenspiel mit dem Grid und dem dahinterliegenden Backend. Während sie auf der Oberfläche als zentrale Eingabekomponente für alle Such- und Filterparameter dient, bildet sie technisch das Bindeglied zwischen der Benutzerinteraktion und der serverseitigen Datenversorgung. Genau in dieser Rolle zeigt sich, wie entscheidend die Entkopplung der Filterlogik von der eigentlichen View für Performance, Wartbarkeit und Erweiterbarkeit ist.

Im ersten Schritt nimmt die SearchBar sämtliche Benutzereingaben entgegen – von globalen Suchtexten über spezifische Shortcode- oder URL-Teile bis hin zu Datumsbereichen, Sortierfeldern und dem Aktivstatus. Diese Werte werden nicht mehr ad hoc in der OverviewView verarbeitet, sondern in strukturierter Form innerhalb der SearchBar gesammelt, validiert und harmonisiert. Dadurch bleiben die Filterzustände konsistent und nachvollziehbar, selbst wenn mehrere Eingabefelder gleichzeitig verändert werden.

Der zweite Schritt ist das Zusammenspiel mit dem Grid. Sobald ein relevanter Filterwert geändert wird, informiert die SearchBar die OverviewView über den aktualisierten Zustand. Diese wiederum löst eine Aktualisierung des Grids aus, ohne selbst die einzelnen Filterkriterien kennen oder verarbeiten zu müssen. Das Grid ruft daraufhin über seinen DataProvider das Backend auf und erhält eine gefilterte Teilmenge der Daten – entsprechend der Kriterien, die aus der SearchBar stammen. So entsteht ein klar getrenntes und dennoch eng verzahntes System aus Eingabe, Datenabruf und Präsentation.

Auf der Backend-Seite zeigt sich der Vorteil des strukturierten Filteraufbaus besonders deutlich darin, wie die OverviewView ihren DataProvider konfiguriert. Die SearchBar liefert dabei immer ein konsistentes Filterobjekt, das direkt in die Backend-Aufrufe einfließt. Zentral dafür ist die Initialisierung des DataProviders in der 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);
}

Hier wird deutlich, wie eng Grid, SearchBar und Backend zusammenspielen: Der DataProvider berechnet auf Basis der aktuellen Paging-Informationen und der Seitengröße die effektiven Abfrageparameter, lässt sich anhand der SearchBar ein UrlMappingListRequest bauen und übergibt dieses Objekt direkt an den URLShortenerClient. Für die Zählfunktion wird ebenfalls die SearchBar verwendet, diesmal ohne Paging-Parameter, um die Gesamtzahl der Einträge zu bestimmen.

Der Vorteil dieser Struktur liegt darin, dass die OverviewView selbst keine Kenntnis über die Details der Filterfelder haben muss. Sie delegiert die gesamte Filterzusammenstellung an die SearchBar und konzentriert sich nur noch darauf, Grid und Paging zu steuern. Änderungen an den Filtern – etwa zusätzliche Kriterien oder geänderte Standardwerte – können vollständig in der SearchBar vorgenommen werden, ohne den DataProvider oder die View anpassen zu müssen.

Insgesamt zeigt das Zusammenspiel von SearchBar, Grid und Backend ein sauber orchestriertes Datenflussmodell: Benutzer ändern einen Filter, die SearchBar erzeugt eine eindeutige Suchanfrage, der DataProvider fordert über den URLShortenerClient die passenden Daten an, und das Grid präsentiert das Ergebnis. Dieser durchgängige, klar strukturierte Ablauf macht die gesamte Oberfläche deutlich stabiler, verständlicher und reaktionsfreudiger.

Cheers Sven

Total
0
Shares
Previous Post

Adventskalender 2025 – Aktiv-/Inaktiv-Modell – Teil 2

Next Post

Adventskalender 2025 – Komponenten extrahieren – Teil 2

Related Posts