TabPanel mit Tapestry5 und Bootstrap

Heute gibt es mal wieder einen Artikel aus der Entwicklerkiste.

Für eine derzeit von mir entwickelte Webanwendung möchte ich gern Daten thematisch sortiert in einem TabPanel anzeigen. Die von mir für das Layout verwendete Bibliothek Twitter Bootstrap liefert bereits das Stylesheet und die Javascript-Funktionen mit. Nun gilt es nur noch, diese mit dem ebenfalls verwendeten Tapestry-Framework zu verheiraten. Dies muß im wesentlichen geschehen, um die Informationen des clientseitig ausgewählten Tabs nicht zu verlieren. Nichts liegt also näher, eine eigene Tapestry-Komponente mit anhängendem Template zu entwickeln. Ich habe zwar zunächst Google-Consulting betrieben und bin auch in der Tat auf fertige Lösungen gestoßen (u.a. ChenilleKit), aber die passen entweder nicht ins Layout oder sind einfach zu übertrieben… Und eine solche Komponente selbst zu entwickeln, ist leichter, als ich zunächst angenommen hatte.

Gedanken im Voraus

Wie soll die Komponente funktionieren? Nun, am einfachsten wäre es, wenn man innerhalb einer Art Container-Tag einfach die Blöcke für die einzelnen Tabs definieren könnte, ähnlich wie die Overrides im Tapestry-Grid. Die Blöcke selbst sind nicht das Problem, allerdings müssen die Anzahl und auch die Namen/IDs derselben für das Rendering bereits von Anfang an bekannt sein. Ich kann mich irren, habe aber keinen Weg gefunden, die genaue Anzahl von Blöcken innerhalb eines Tags herauszufinden. Also benötigt unsere Komponente mindestens einen Parameter für die Tabs. Hierzu genügt eine kommagetrennte Liste. Innerhalb des Tags folgen dann die Blöcke für die einzelnen Tabs, wobei jeder Block den Namen eines korrespondierenden Eintrags aus dem Parameter haben muß.

Das Template

In der Template-Datei wird der HTML-Code zur Darstellung gespeichert. Der Code folgt im wesentlichen den Vorgaben für Tabs durch Bootstrap und ist – was die Erweiterung um Tapestry-spezifische Angaben anbelangt – überraschend einfach und übersichtlich.





In einer HTML-Liste werden zunächst die Tabs selbst dargestellt. Hierzu wird mit einem Tapestry-Loop über die Eigenschaft tabArray iteriert und der verarbeitete Name in der Eigenschaft loopTab zwischengespeichert. Jeder Listeneintrag erhält eine Style-Klasse, die – wenn es sich um den aktiven Tab handelt – den Wert active hat. Außerdem wird ein EventLink ausgegeben, um der Komponente die Möglichkeit zu geben, bei der Änderung des aktiven Tabs durch den Benutzer diesen auch serverseitig zwischenspeichern zu können. Der Parameter anchor wird auf die ID des Tabs gesetzt, was von Bootstrap benötigt wird. Ebenso wird der Name als Kontext an den Event-Handler übergeben. Die Verwendung des EventLinks hat natürlich eine wichtige Auswirkung: Entgegen der Original-Bootstrap-Vorgehensweise wird hier ein zusätzlicher Request auf dem Server erzeugt. Daher ist auch der gesamte div-Block im Template mit einer Tapestry-Zone umgeben: Dies hält den Request klein.
Innerhalb des inneren divs wird schließlich der Inhalt jedes Tabs ausgegeben. Auch hier wird wieder über die Eigenschaft tabArray iteriert. Wichtig sind die Style-Klasse sowie die ID für jeden div. Die Ausgabe des Inhalts wird dann an den entsprechenden Block delegiert, der durch Aufruf der Methode getLoopTabBlock() ermittelt wird.

Die Komponentenklasse

Die Komponentenklasse enthält alle Eigenschaften bzw. Getter-Methoden, die vom Template aus angefordert werden. Folgende Eigenschaften sind notwendig:

public class Tabbable {
	
    @InjectComponent("tabbableZone")
    private Zone zone;

    @Inject
    private ComponentResources resources; 

    @Persist
    private String activeTab;

    /** Kommaseparierte Namen der Tabs. */
    @Parameter(allowNull=false,required=true,defaultPrefix=BindingConstants.LITERAL)
    private String tabs;

    private String loopTab;

    // Getter und Setter für loopTab
    ...
}

Die wichtigsten Eigenschaften sind activeTab und tabs. In ersterem wird der Name des aktuell ausgewählten Tabs gespeichert. Durch die Annotation @Persist wird Tapestry dazu angewiesen, den Wert zwischenzuspeichern. Dadurch bleibt die Information über den zuletzt aktiven Tab zwischen Requests erhalten. Die Eigenschaft kann außerdem mit @Property annotiert oder über ein Getter-/Setter-Paar zugänglich gemacht werden, wodurch ein explizites Setzen des aktiven Tabs von außen möglich würde.
Die Eigenschaft tabs ist als Parameter annotiert. Dieser muß zwingend angegeben werden und erwartet eine kommagetrennte Liste von Bezeichnern für die darzustellenden Tabs (also z.B. „tab1,tab2,tab3„).
Die ComponentResources werden zur Auflösung von Nachrichten sowie der darzustellenden Blöcke benötigt.
In der Eigenschaft loopTab wird, wie bereits angesprochen, der aktuelle Name des in einer Schleife verarbeiteten Tabs gesichert. Für diese ist denn auch ein Getter-/Setter-Paar notwendig.

Neben den Eigenschaften sind folgende Methoden in der Komponentenklasse enthalten:

public class Tabbable {

        ...

	/**
	 * Liefert die Namen der Tabs als Array.
	 * @return Array mit den Namen der Tabs
	 */
	public String[] getTabArray() {
		return tabs.split(",");
	}

	/**
	 * Liefert die CSS-Klasse für die Markierung des aktiven Tabs.
	 * @return CSS-Klasse
	 */
	public String getLoopTabClass() {
		return loopTab.equals(activeTab) ? "active" : "";
	}

	/**
	 * Liefert den Titel des aktuell bearbeiteten Tabs.
	 * @return Titel des aktuell bearbeiteten Tabs
	 */
	public String getLoopTabTitle() {
		Messages messages = 
				resources.getPage().getComponentResources().getMessages();
		return messages.get(resources.getId()+ "."+loopTab+".title");
	}

	/**
	 * Liefert den zu rendernden Block des aktuell bearbeiteten Tabs.
	 * @return Block des aktuell bearbeiteten Tabs
	 */
	public Block getLoopTabBlock() {
		return resources.getBlockParameter(loopTab);
	}

	/**
	 * Event-Handler für das Klicken auf einen Tab. Sichert den angeklickten
	 * Tab und aktualisiert die Anzeige.
	 * @param tab angeklickter Tab
	 * @return Inhalt der zu aktualisierenden Zone
	 */
	public Object onToggle(String tab) {
		this.activeTab = tab;
		return zone.getBody();
	}
}

Die Methode getTabArray() zerlegt den Wert des Parameters tabs in ein Array, um so die Iteration im Template zu ermöglichen.
getLoopTabClass() vergleicht den aktiven Tab mit dem aktuell bearbeiteten und liefert gemäß Bootstrap die CSS-Klasse active, sofern beide Werte identisch sind.
In der Methode getLoopTabTitle() wird versucht, den Titel für einen Tab zu ermitteln. Um die Verwendung der Komponente zu vereinfachen, wird in den Message-Ressourcen nach einem Schlüssel der Form komponenten-id.tab-id.title gesucht.
Die Methode getLoopTabBlock() liefert den Block des Tabs für das Rendering. Hier wird einfach in den Ressourcen nach einem informellen Parameter mit dem Tab-Namen gesucht.
Bei onToggle(String) handelt es sich schließlich um den Event-Handler für den Klick auf einen Tab. Hier wird einfach der ausgewählte Tab in der persistenten Eigenschaft activeTab gesichert und eine Aktualisierung der Zone durch Rückgabe des Bodys derselben ausgelöst.

Der Einsatz

Der Einsatz im Template einer Tapestry-Seite sieht wie folgt aus:


    
        Hier steht der Inhalt von Tab 1.
    
    
        Hallo, ich bin Tab 2.
    
    
        ... und Tab 3.
    

Wichtig ist neben der Angabe der kommaseparierten Liste eindeutiger Namen für Tabs die Angabe der korrespondierenden Blöcke. Im Beispiel werden über den Parameter t:tabs="tab1,tab2,tab3" genau drei Tabs definiert und für jeden die drei Tabs folgt ein Parameterblock mit gleichem Namen: p:tab1 bis p:tab3. Angegeben werden die Blöcke in Form von informellen Parametern mit Hilfe des parameter-Namensraums p, gefolgt vom Namen des Tabs wie im Parameter angegeben.
Die Message-Ressourcen für die Titel der drei Tabs sehen zu guter letzt dann wie folgt aus:

tabpanel.tab1.title=Tab 1
tabpanel.tab2.title=Tab 2
tabpanel.tab3.title=Tab 3

Die Schlüsselzusammensetzung ist abgeleitet aus der ID der Komponente (tabpanel), dem Namen des Tabs sowie dem statischen Teil title.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.