Teil 6: Aufbau der Anwendung

Backend meets Frontend Reloaded: Layout-Mechanismus in Vaadin Flow

Sven Ruppert

© Shutterstock / HelenField & bkf (modifiziert)

Im letzten Teil der Serie haben wir ein Grundgerüst für die Anwendung erstellt. Nun sind wir in dieser Serie an der Stelle angekommen, an der wir uns über einen grundsätzlichen Aufbau der Anwendung Gedanken machen können. Um sich mit dem Thema zu beschäftigen, benötigt man zunächst einmal mehr Informationen darüber, wie der Layout-Mechanismus in Vaadin Flow funktioniert.

Die Anforderungen

Eine Webanwendung, die in einem typischen Geschäftsprozess eingesetzt wird, hat meistens eine Grundstruktur, in der die verfügbaren Einheiten gegliedert werden. In diesem Zusammenhang kann man sich Menüs, Toolbars, Statusbalken und Arbeitsbereiche vorstellen. Heute werden wir uns ansehen, wie man solch eine Struktur in Vaadin Flow aufbauen kann und wie dies sich auf die Definition der Navigation auswirken wird.

Vorbereitungen

In der letzten Folge dieser Serie haben wir eine Klasse mit dem Namen MainView verwendet. Darin enthalten war lediglich ein Label mit dem sinnhaften Inhalt Main View. An dieser Stelle möchte ich nun ansetzen, um dieser Ansicht ein wenig Leben einzuhauchen. Wenn wir nun also eine Menüleiste einbauen wollen, so stellt sich direkt die Frage, an wievielen Stellen der Anwendung man diese zu sehen bekommt.

Sicher ist, dass es wohl eine der Komponenten sein wird, die immer auf dem Bildschirm sein werden, vorausgesetzt die Auflösung ist an dieser Stelle bei der Verwendung konstant. Das Thema Responsive Design werden wir an späterer Stelle angehen.

Der Name der zu erzeugenden Klasse ist MenuBar. Um der Menüleiste einige Einträge spendieren zu können, erzeugen wir zusätzlich einige Navigationsziele. Die Namen der Klassen sind ViewA und ViewB.

@Route(value = ViewA.NAV_VIEW_A)
public class ViewA extends Composite<Div> {
  public static final String NAV_VIEW_A = "ViewA";

  public ViewA() {
    getContent().add(new Span("View A"));
  }
}

Innerhalb der Menüleiste sollen nun drei Links angezeigt werden. Allerdings beginnt genau zu diesem Zeitpunkt oft eine indirekte Annahme, die ich hier expliziet ausschließen möchte: Die Menüleiste ist meines Erachtens nach kein Teil der Layoutstruktur, sondern ein Element, das in dem Layout positioniert werden soll. Kommen wir aber erst einmal zu dem Menü selbst: Da die Elemente im Menü horizontal angeordnet werden sollen, kann als Basis dafür Composite<HorizontalLayout> verwendet werden.

public class MenuBar extends Composite<HorizontalLayout> {
  public MenuBar() {
    getContent().add(new RouterLink("Home" , MainView.class));
    getContent().add(new RouterLink("ViewA" , ViewA.class));
    getContent().add(new RouterLink("ViewB" , ViewB.class));
  }
}

Natürlich kann man von hier aus auch schon auf den I18NProvider zugreifen.

public class MenuBar extends Composite<HorizontalLayout> {

  public static final String MENU_BAR_HOME = "menubar.home";
  public static final String MENU_BAR_VIEW_A = "menubar.view-a";
  public static final String MENU_BAR_VIEW_B = "menubar.view-b";

  public MenuBar() {
    getContent().add(new RouterLink(getTranslation(MENU_BAR_HOME) , MainView.class));
    getContent().add(new RouterLink(getTranslation(MENU_BAR_VIEW_A) , ViewB.class));
    getContent().add(new RouterLink(getTranslation(MENU_BAR_VIEW_B) , ViewB.class));
  }
}

Die Einträge dazu habe ich in der Root-ResouceBundle untergebracht, da eine sinnvolle Übersetzung an der Stelle nicht gegeben ist.

Das Grundgerüst

Kommen wir nun zum Grundgerüst selbst. Innerhalb der Anwendung soll das Menü immer oben in der Zeile untergebracht sein. Sicherlich kann man mittels Vererbung versuchen, eine Basisklasse zu erzeugen, die im späteren Verlauf die Definition der Grundstruktur bereitstellt. Hiervon möchte ich allerdings abraten, da man auch an dieser Stelle sehr schön diese Verantwortlichkeiten trennen kann.

Der prinzipielle Aufbau ist mit dem einer gewöhnlichen Komponente identisch. Eingebettet in Composite<VerticalLayout> kann man sowohl die Komponente der Menüleiste als auch den Platzhalter für den Content definieren.

Achtung! Wer nochmal nachlesen möchte, was es mit dem Composite auf sich hat, der sollte einen Blick in die Dokumentation werfen.

Beides wird natürlich mit einer ID versehen, damit später einfach mittels Selenium darauf zugegriffen werden kann. Das Testen der Anwendung werden wir ausführlich zu einem späteren Zeitpunkt behandeln.

public class LayoutWithMenuBar extends Composite<VerticalLayout> {

  public static final String LAYOUT_CONTENT_ID = "layout-content";
  public static final String LAYOUT_MENUBAR_ID = "layout-menubar";

  private final MenuBar menuBar = new MenuBar();
  private final Div content = new Div();

  public LayoutWithMenuBar() {
    menuBar.setId(LAYOUT_MENUBAR_ID);
    content.setId(LAYOUT_CONTENT_ID);
    getContent().add(menuBar, content);
  }
}

Wenn dieser Zustand erreicht worden ist, beginnt der spezifische Teil bei der Erstellung eines Layouts. Gemeint ist der Teil, der es später ermöglicht, das Routing innerhalb dieser Teilkomponenten abzubilden. Was auf jeden Fall schon aufgefallen sein kann, ist die Tatsache, dass keine Möglichkeit vorgesehen worden ist. den Inhalt der Komponente content zu setzen. Wie soll nun dort etwas eingebracht werden?

Die Lösung liegt in der Abbildung des Lebenszyklus‘ selbst. Hierzu muss man die Klasse LayoutWithMenuBar mit implements RouterLayout versehen werden. Um dann auf den Zeitpunkt reagieren zu können, zu dem das Element für den Inhalt gesetzt werden kann, ist die Methode showRouterLayoutContent zu überschreiben. Als erstes wird der aktuelle Inhalt von dem Attribut content entfernt. Nachfolgend kann der Inhalt mit den neuen Elementen gesetzt werden, falls vorhanden.

@Override
public void showRouterLayoutContent(HasElement hasElement) {
  logger().info("showRouterLayoutContent - LayoutWithMenuBar");
  content.removeAll();
  if(hasElement != null){
    requireNonNull(hasElement.getElement());
    content.getElement().appendChild(hasElement.getElement());
  }
}

Aktivieren des Layouts

Die Implementierung ist damit verwendbar, muss allerdings noch aktiviert werden. Die Verbindung zwischen Route und dem Layout findet in der Annotation @Route statt. Nachfolgend zu dem Attribut value kommt nun noch die Definition, welches Layout der Bezugspunkt sein wird. In der Annotation Route befindet sich für diesen Zweck das Attribut layout, das als Parameter eine Klasse erwartet, die das Interface RouterLayout implementiert. In unserem Beispiel ist es somit: layout = LayoutWithMenuBar.class

@Route(value = ViewA.NAV_VIEW_A,layout = LayoutWithMenuBar.class)
public class ViewA extends Composite<Div> {
  public static final String NAV_VIEW_A = "ViewA";

  public ViewA() {
    getContent().add(new Span("View A"));
  }
}

Es ist wichtig zu wissen, dass die Angabe eines Themes nun an dem Layout zu erfolgen hat und nicht mehr an den Views selbst. Sollte man das nicht beachtet haben, wird man mit einer Exception vom Typ InvalidRouteLayoutConfigurationException belohnt.

Nested Layout

Nicht immer reicht eine so einfache Struktur aus. Wie kann man aber ein Layout in ein anderes einbetten? Auch hier gilt es wieder zu erreichen, dass jedes Layout so weit wie möglich unabhängig ist. Um nun beispielhaft ein geschachteltes Layout aufzubauen, wollen wir um das vorhandene Layout nun noch ein weiteres legen, das lediglich ein Span über und unter dem bisher vorhandenen anbringt.

Als Ausgangspunkt wird eine Klasse mit dem Namen MainLayout erzeugt; auch in diesem Fall wieder als Composite<VerticalLayout> und als Implementierung des Interfaces RouterLayout.

@Theme(value = Lumo.class, variant = Lumo.DARK)
public class MainLayout
    extends Composite<VerticalLayout>
    implements RouterLayout, HasLogger {
    
  public static final String LAYOUT_CONTENT_ID = "main-layout-content";
  //Component to delegate content through.
  private Div content = new Div();

  public MainLayout() {
    content.setId(LAYOUT_CONTENT_ID);
    getContent().add(
        new Span("TOP"),
        content,
        new Span("Bottom"));
  }

  @Override
  public void showRouterLayoutContent(HasElement hasElement) {
    logger().info("showRouterLayoutContent - MainLayout");
    content.removeAll();
    if(hasElement != null){
      requireNonNull(hasElement.getElement());
      content.getElement().appendChild(hasElement.getElement());
    }
  }
}

Um die Layouts zu verschachteln, werden die einzelnen Teillayouts and ihren jeweiligen Vater gebunden. Das passiert mittels der Annotation @ParentLayout. In unserem Beispiel ist die folgende Annotation an der Klasse LayoutWithMenuBar zu verwenden: @ParentLayout(value = MainLayout.class).

Fazit

Hiermit haben wir jetzt die Möglichkeit, beliebig komplex verschachtelte Layouts zu konstruieren. Selbstverständlich gilt auch dabei wieder, dass es nicht übertrieben werden sollte. Wo da die Grenze liegt, ist allerdings (wie immer) eine recht individuelle Metrik. Im nächsten Teil werden wir uns damit beschäftigen, wie man einen Anwendungsrahmen basierend auf Vaadin AppLayout aufbauen kann.

Das Beispiel zu diesem Teil ist auf GitHub zu finden.

Wer Fragen und Anmerkungen hat, meldet sich am besten per Twitter an @SvenRuppert oder direkt per Mail an sven.ruppert@gmail.com

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: