cs.Tutorial();

WebComponents in einem Java Webframework einsetzen

  • WebComponents
  • JavaScript
  • Java
  • Wicket

Unter dem Begriff Web Components wird eine Reihe unterschiedlicher Webtechnologien zusammengefasst, die es ermöglichen, benutzerdefinierte und wiederverwendbare HTML-Elemente zu implementieren. Ihre Funktionalität und Styling sind in sich gekapselt und damit vollständig getrennt vom restlichen Code der Webanwendungen, in denen sie eingesetzt werden. Sie funktionieren in allen modernen Webbrowsern und lassen sich damit in allen Webframeworks, die HTML nutzen, einsetzen. Auch in einem Java-basierten Server-Side-Rendering-Webframework, wie Apache Wicket, ist dies möglich. In diesem Artikel wird beschrieben, wie eine einfache WebComponent unter Einsatz entsprechender Webtechnologien zunächst in einem JavaScript-Projekt entwickelt und später in eine vorhandene Webanwendung integriert werden kann. Dabei werden sowohl die notwendigen Anpassungen am bestehenden Build als auch die unterschiedlichen Ansätze zur Integration mit dem eingesetzten Webframework vorgestellt.

Die neue WebComponent

Die Reise beginnt mit der Entwicklung einer einfachen WebComponent. Ihre Aufgabe soll darin liegen, einen Zeitabstand vom aktuellen bis zum benutzerdefinierten Zeitstempel zu berechnen und ihn als formatierte Zeichenfolge in einer festgelegten Sprache auszugeben. Diese Darstellung soll sich in einem Intervall von einer Minute aktualisieren und damit immer den aktuellen Abstand anzeigen. Das Format dieser Zeichenfolge soll zum einen davon abhängig sein, ob der angegebene Zeitstempel sich in der Vergangenheit oder Zukunft befindet, und zum anderen, wie weit dieser vom aktuellen Zeitstempel entfernt ist. In Tabelle 1 werden die zeitlichen Abstände und die entsprechenden Formate gegenübergestellt.

BezugsdatumFormat
Am selben Tag in der Vergangenheitvor <X> <Zeiteinheiten>
Am selben Tag in der Zukunftin <X> <Zeiteinheiten>
Am Tag zuvorGestern um <Uhrzeit> Uhr
Am nächsten TagMorgen um <Uhrzeit> Uhr
In der letzten WocheLetzten <Wochentag> um <Uhrzeit> Uhr
In der nächsten Woche<Wochentag> um <Uhrzeit> Uhr
Sonst10.03.2019
Tabelle 1: Ausgabeformate

Zur Berechnung von Zeitabständen wird Moment.js verwendet – eine JavaScript-Bibliothek zum Parsen, Validieren, Manipulieren und Berechnen von Zeit- und Datumswerten.

Spezifikationen

WebComponents setzen auf die Standards Custom Elements, Shadom DOM, ES Module und HTML Template auf. Zusammengefasst ermöglichen sie die Erweiterung von HTML mit neuen Komponenten mit gekapseltem Styling und benutzerdefiniertem Verhalten. Bis auf das Feature HTML Template werden in der anknüpfenden Implementierung alle Standards verwendet, weshalb sie im Folgenden kurz erläutert werden:

  • Custom Elements: Diese Spezifikation umfasst einen Satz von JavaScript APIs, die es ermöglichen, benutzerdefinierte Elemente und deren Verhalten zu definieren und sie überall im HTML zu verwenden.
  • Shadow DOM: Diese Spezifikation definiert die Kapselung des Stylings und des Markups von Web Components.
  • ES Module: Seit Version ES6 sind Module im JavaScript-Standard enthalten und bilden eine Basis für die strukturierte und gekapselte Entwicklung von JavaScript.
  • HTML Template: Das Feature HTML Template ist ein Mechanismus, mit dem benutzerdefinierte Markup-Vorlagen im HTML-Dokument angegeben werden können, die zwar Client-seitig evaluiert aber nicht gerendert werden. Erst bei Bedarf können sie gezielt via JavaScript instanziiert werden.

Das Grundgerüst

Vorbereitend wird ein neues JavaScript-Projekt in einem neuen Verzeichnis mit einem beliebigen Namen, wie zum Beispiel WebComponents, angelegt. In diesem Projekt soll die neue WebComponent TimeGap entwickelt werden.

Um ein neues Custom Element zu implementieren, sind zwei Dinge notwendig: ein Klassenobjekt, welches die Klassensyntax des ES6-Standards verwendet, und dessen Registrierung über den Aufruf der define-Methode auf dem globalen customElements-Objekt. Um die Implementierung von der Registrierung zu trennen werden im Unterordner src/ die zwei JavaScript-Dateien time-gap.js und index.js erstellt. In der ersten wird das Klassenobjekt der WebComponent implementiert, in der zweiten findet dessen Deklaration als Custom Element unter dem Tag-Namen time-gap statt. Die Datei index.js stellt zugleich das zentrale Modul dar, das in das HTML-Dokument eingebunden wird, in dem die Web Component verwendet werden soll. Listing 1 zeigt die Inhalte der beiden Dateien.

// src/time-gap.js
export class TimeGap extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
this.label = document.createElement('span');
this.label.innerText = 'vor nicht allzu langer Zeit...';
shadow.appendChild(this.label);
}
}
// src/index.js
import { TimeGap } from './time-gap.js';
customElements.define('time-gap', TimeGap);
Listing 1: Der initiale Inhalt von time-gap.js und index.js

Die WebComponent beinhaltet zunächst noch keine Logik. Ihr Mark-up besteht aus einem span-Element, das an die Shadow-Root angehängt und initial mit einem Platzhaltertext versehen wurde. Um die Nutzung auf einer Website zu demonstrieren, wird im Hauptverzeichnis eine index.html erstellt und das Modul index.js, wie im Listing 2 abgebildet, eingebunden. Der Inhalt des body-Tags im HTML-Dokument besteht dabei lediglich aus der Verwendung des neuen time-gap-Tags.

<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
<!-- index.js als ES-Modul einbinden -->
<script type="module" src="src/index.js"></script>
</head>
<body>
<!-- die neue Web Component verwenden -->
<time-gap></time-gap>
</body>
</html>
Listing 2: index.html

Damit die importierten ES-Module nachgeladen werden können, wird zusätzlich ein Webserver benötigt. Zu reinen Entwicklungszwecken von WebComponents empfiehlt sich die Verwendung eines sogenannten Live-Servers. Dieser überwacht den gesamten Quellcode und lädt automatisch Inhalte nach, sobald sich diese geändert haben. Ein Live-Server kann, sofern Nodes.js auf dem lokalen System verfügbar ist, über den Paketmanager NPM installiert und über die Kommandozeile im Hauptverzeichnis des jeweiligen Projekts mit dem Befehl live-server gestartet werden.

Wird die index.html nun in einem modernen Browser aufgerufen, der die oben erwähnten Features implementiert, so erscheint der Platzhaltertext, der im Konstruktor der WebComponent hinzugefügt wurde.

Die Schnittstelle

Im nächsten Schritt geht es darum, der Web Component etwas Leben einzuhauchen: Die statische Ausgabe soll durch eine dynamische ersetzt werden. Als Vorbereitung dafür muss die Schnittstelle der neuen Komponente in Form von Tag-Attributen definiert werden. Insgesamt werden zwei Attribute benötigt: from und locale. Das Attribut from soll als Bezugspunkt für die Berechnung des Zeitabstands dienen. Es soll in einem nach dem internationalen Standard ISO-8601 gültigen Datumsformat angegeben werden. Mit locale sollen Anwender die Sprache für die Ausgabe festlegen können. Die Belegung könnte zum Beispiel wie folgt aussehen:

<time-gap from="2019-01-03T14:45" locale="de"></time-gap>

Einsatz von moment.js

Für die Berechnung und die formatierten Ausgaben des Zeitabstands soll die JavaScript-Bibliothek Moment.js verwendet werden. Damit diese Abhängigkeit (und später andere) nicht manuell verwaltet werden muss, empfiehlt sich die Erstellung einer package.json-Datei. Es wird also höchste Zeit, um die Installation von Node.js auf der lokalen Entwicklungsumgebung vorzunehmen.

Ist Node.js auf dem System verfügbar, so kann der enthaltene Paketmanager NPM dazu benutzt werden, um eine neue package.json-Datei zu generieren. Für diesen Schritt muss über die Kommandozeile im Hauptverzeichnis des Projekts der Befehl npm init -y ausgeführt werden. Mit dem Befehl npm install moment --save kann anschließend die JavaScript-Bibliothek Moment.js aus der NPM-Registry heruntergeladen und als Abhängigkeit in die package.json aufgenommen werden. Damit wurde Moment.js im Projekt installiert und kann über die import-Anweisung in das Modul time-gap.js importiert und dort verwendet werden.

Die Programmierschnittstelle von Moment.js ist sehr umfangreich. Sie stellt Methoden zur Verfügung, die es erlauben, auf unterschiedliche Art und Weise mit Zeit- und Datumswerten zu arbeiten. Für die Umsetzung von TimeGap sind vor allem die Methoden isSame, fromNow und calendar von Bedeutung. Die Methode isSame ermöglicht die Abfrage, ob das als Argumente übergebene Datum sich im selben Jahr Monat oder Tag befindet, wie das Datum, mit dem das moment-Objekt initialisiert wurde. Die Methode fromNow gibt den zeitlichen Abstand vom jetzigen Zeitpunkt bis zu dem Zeitpunkt, mit welchem das moment-Objekt initialisiert wurde. Das Ergebnis kommt der Antwort auf die Frage „Wie lange ist es her?“ sehr nah und kann beispielsweise „vor einer Stunde und 30 Minuten“ lauten. Mit calendar verhält es sich ähnlich, jedoch mit dem Unterschied, dass hier die Frage „Wann“ beantwortet wird. In der Tabelle 2 sind die möglichen Ergebnisse dieser Methode dargestellt.

BezugsdatumBeispielhafte Ausgabe
In der letzten WocheLetzten Montag um 15:30 Uhr
Am Tag zuvorGestern um 15:30 Uhr
Am selben TagHeute um 15:30 Uhr
Am nächsten TagMorgen um 15:30 Uhr
In der nächsten WocheMittwoch 15:30 Uhr
Sonst10.03.2019
Tabelle 2: Die möglichen Ausgaben der Methode moment#calendar

Den Zeitabstand ermitteln

Für die Ermittlung des aktuellen Zeitabstands wird eine Klassenmethode mit dem Namen elapsedTime definiert, deren Implementierung in Listing 3 abgebildet ist.

import moment from 'moment';
export class TimeGap extends HTMLElement {
//...
static elapsedTime(from, locale) {
const fromMoment = moment(from).locale(locale);
return fromMoment.isSame(new Date(), 'day') ?
fromMoment.fromNow() :
fromMoment.calendar();
}
}
Listing 3: time-gap.js – Implementierung der Methode elapsedTime

Die Methode bekommt die zwei Argumente from und locale übergeben, womit ein neues moment-Objekt initialisiert und in der Variable fromMoment gespeichert wird. Damit wurde der Bezugspunkt der Zeitberechnung als auch die Sprache für die Ausgabe festgelegt.

In einer Bedingung wird anschließend festgestellt, ob es sich bei dem übergebenen Zeitstempel, um einen Zeitstempel von heute handelt. Das gelingt, indem die Methode isSame auf dem moment-Objekt mit einer neuen Datum-Instanz new Date() und der Zeichenfolge day aufgerufen wird. Trifft diese Bedingung zu, dann wird der zeitliche Abstand mit der Methode fromNow berechnet. Andernfalls kommt die calendar-Methode zum Tragen und liefert das Ergebnis.

In Listing 4 ist die nächste Methode implementiert, die für das Aktualisieren des Inhalts vom span-Element zuständig ist. Die vom Anwender angegebenen Attribute from und locale werden ausgelesen und als Argumente an den Aufruf der Methode elapsedTime weitergereicht. Das Ergebnis wird auf der Benutzeroberfläche zur Anzeige gebracht, indem es der Eigenschaft innerText des span-Elements zugewiesen wird.

//...
updateTimeGap() {
const from = this.getAttribute('from');
const locale = this.getAttribute('locale');
this.label.innerText = TimeGap.elapsedTime(from, locale);
}
//...
Listing 4: time-gap.js – Implementierung der Methode updateTimeGap

Nun fehlt noch der Aufruf der updateTimeGap-Methode in regelmäßigen Abständen von einer Minute. Der initiale Zeitpunkt dafür ist genau der, an dem die Website zum ersten Mal geladen wird. Für dieses Ereignis und noch ein paar andere verwenden CustomElements die sogenannten Lifecycle-Callbacks – Methoden, die zu bestimmten Phasen in ihrem Lebenszyklus automatisch aufgerufen werden. Die hier relevanten Methoden lauten connectedCallback und disconnectedCallback. Die erste wird aufgerufen, wenn das CustomElement in das DOM eingehängt, die zweite, wenn es aus dem DOM wieder entfernt wird.

Die Klasse TimeGap wird um die beiden Methoden erweitert. In der connectedCallback wird die updateTimeGap initial aufgerufen und anschließend als Argument an die auf dem Window-Objekt definierte setInterval-Methode mit 60.000 Millisekunden übergeben, um sich im Intervall von einer Minute aufrufen zu lassen. Da sie dadurch als globale Funktion aufgerufen wird, muss die aktuelle Instanz der Klasse als ihr Ausführungskontext mit bind(this) explizit angegeben werden. Die als Ergebnis von setInterval gelieferte ID wird in einer entsprechenden Instanzvariable gespeichert. Sie wird in der disconnectedCallback benutzt, um das Intervall mit der globalen clearInterval-Methode wieder entfernen zu lassen. Damit ist TimeGap vollständig implementiert. Listing 5 zeigt den gesamten Quelltext der neuen WebComponent.

import moment from 'moment';
export class TimeGap extends HTMLElement {
static elapsedTime(from, locale) {
const fromMoment = moment(from).locale(locale);
return fromMoment.isSame(new Date(), 'day') ?
fromMoment.fromNow() :
fromMoment.calendar();
}
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
this.label = document.createElement('span');
this.label.innerText = 'vor nicht allzu langer Zeit...';
shadow.appendChild(this.label);
}
connectedCallback() {
this.updateTimeGap();
this.intervalId = setInterval(
this.updateTimeGap.bind(this),
60000
);
}
disconnectedCallback() {
clearInterval(this.intervalId);
}
updateTimeGap() {
const from = this.getAttribute('from');
const locale = this.getAttribute('locale');
this.label.innerText = TimeGap.elapsedTime(from, locale);
}
}
Listing 5: time-gap.js – die vollständige Implementierung

Module mit Webpack packen

Um Abhängigkeiten zwischen den Modulen aufzulösen und den benötigten Quelltext zusammengepackt in einer einzelnen JavaScript-Datei auszuliefern, wird Webpack eingesetzt. Damit wird sichergestellt, dass die Web Component keine unnötigen Server-Anfragen stellt, um Skripte, die sie importiert, nachzuladen. Ein nützlicher Nebeneffekt ist dabei, dass der Code in Webbrowsern ausgeführt werden kann, die noch keine Unterstützung für ES-Module bereitstellen.

Folgende Anweisung muss für die Installation von Webpack und des ebenso benötigten Webpack-CLI im Hauptverzeichnis ausgeführt werden:

npm install webpack webpack-cli -save-dev

Um Webpack zu konfigurieren, wird im Hauptverzeichnis eine webpack.config.js mit dem Inhalt aus Listing 6 angelegt.

const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
}
};
Listing 6: Webpack Konfiguration

Neben sämtlichen Konfigurationsmöglichkeiten kann damit vor allem der Einstiegspunkt, der Pfad und der Name der zusammengepackten Datei definiert sowie der Modus für das Packen festgelegt werden. Es werden der Name der Zieldatei und das Verzeichnis, in dem sie abgelegt werden soll, definiert. Diese Änderung bedingt auch die Anpassung des entsprechenden script-Tags in der index.html. Dort muss der neue Pfad zu der gepackten JavaScript-Datei dist/main.js angegeben werden:

<head>
<!--...-->
<script src="dist/main.js"></script>
<head>

Der Modus production sorgt dafür, dass die Ergebnisdatei möglichst klein ausfällt. Sie wird minifiziert, indem der komplette Leerraum und Kommentare entfernt sowie Variablennamen in deutlich kürzere umbenannt werden. Für die Entwicklung ist dieser Modus ungeeignet, weshalb hier der Parameter mode auf development eingestellt wird.

Die WebComponent kann nun getestet werden, indem die Befehle npx webpack und live-server über die Kommandozeile im Hauptverzeichnis nacheinander aufgerufen werden. Mit Webpack wird der gesamte JavaScript-Quellcode zu einer einzigen main.js-Datei zusammengepackt und anschließend vom Live-Server bereitgestellt. Die im Webbrowser automatisch aufgerufene index.html lässt die neue WebComponent das Ergebnis berechnen und wie erwartet anzeigen.

Integration in eine Webanwendung

Im Folgenden wird schrittweise die Integration der entwickelten WebComponent in eine bestehende Webanwendung vorgestellt.

Die Webanwendung Tasks

Die Webanwendung Tasks wurde mit Apache Wicket in der Version 8.1 entwickelt. Das Projekt wird mit Gradle als Build-Management-Tool gebaut und setzt Spring Boot 2 als Entwicklungswerkzeug für Anwendungen mit Spring-Kontext ein. Die Applikation stellt eine einfache Aufgabenliste dar. Es können neue Aufgaben hinzugefügt, bestehende Aufgabe als erledigt markiert oder gelöscht werden. Abbildung 1 zeigt die Benutzeroberfläche der Webanwendung.

Abbildung 1: Die Benutzeroberfläche der Webanwendung Tasks

In jedem Listeneintrag, unterhalb der Aufgabenbeschreibung, wird das Erstellungsdatum des jeweiligen Eintrags ausgegeben. Dieses Datum soll durch die neue WebComponent etwas gesprächiger gemacht werden.

Die WebComponent einbinden

Die initiale Projektstruktur der Webanwendung kann Abbildung 2 entnommen werden.

Abbildung 2: Die initiale Projektstruktur

Im Verzeichnis src/main/webapp sind aktuell die benötigten Stylesheets und Webfonts der Webanwendung abgelegt. In diesem Ordner wird ein neues Unterverzeichnis js erzeugt, in welches die zuvor erstellten JavaScript-Dateien time-gap.js und index.js hineinkopiert werden. Die CSS-Datei der Webanwendung wird bereits in der BasePage-Klasse, von der alle Seiten der Applikation erben, in der Methode renderHead(response: HeaderResponse) eingebunden. Mit ihr kann in Wicket der HTML-Head der angefragten Seite mit unterschiedlichen Header-Tags dekoriert werden. Diese Methode wird, wie in Listing 7 abgebildet, erweitert. An die Response wird dabei ein JavaScriptHeaderItem angehängt, mit dessen Hilfe die mit der relativen URL dist/js/main.js angegebene JavaScript-Datei geladen werden soll. Im weiteren Verlauf wird im Build der Webanwendung sichergestellt, dass diese Datei dort auch tatsächlich landet.

public class BasePage extends WebPage {
@Override
public void renderHead(IHeaderResponse res) {
super.renderHead(res);
res.render(CssHeaderItem.forUrl("dist/css/main.css"));
res.render(JavaScriptHeaderItem.forUrl("dist/js/main.js"));
}
}
Listing 7: BasePage.java – benutzerdefiniertes JavaScript in Wicket einbinden

Node.js als Gradle-Plug-In

Wenn die einzelnen JavaScript-Module weiterhin zu einer einzelnen JavaScript-Datei gepackt und die externe Bibliothek Moment.js nicht in das Quellcode-Repository des Projekts aufgenommen werden soll, dann wird das Build-Management um die Verwendung von NPM und Webpack nicht herumkommen. Die JavaScript-Laufzeitumgebung Node.js wird also auch hier weiterhin benötigt.

Da das Build-Management nicht von der lokalen Node.js-Umgebung abhängig gemacht werden soll, empfiehlt sich stattdessen der Einsatz des Gradle-Plug-ins Node. Dieses Plug-in ermöglicht die lokale Installation und Verwendung einer Projekt-spezifischen Node.js-Version, ohne die JavaScript-Laufzeitumgebung global auf dem System des Entwicklers installieren zu müssen.

Das Node-Plug-in wird eingebunden, indem die build.gradle-Datei um den plugins-Block mit der vollqualifizierten Plug-in-ID erweitert wird (Listing 8).

plugins {
id "com.moowork.node" version "1.2.0"
}
Listing 8: build.gradle - das Node-Plug-In einbinden

Diese Einbindung stattet den Gradle-Build zusätzlich mit ein paar hilfreichen Tasks aus, die für die Arbeit mit Node-Projekten unabdingbar sind. Der wichtigste Task dabei ist npmInstall, mit dem alle in der package.json als Abhängigkeit definierten NPM-Pakete heruntergeladen und im Verzeichnis node_modules installiert werden. Im Hauptverzeichnis des Projekts wird eine neue package.json-Datei mit dem Inhalt aus Listing 9 angelegt.

{
"name": "task-components",
"version": "1.0.0",
"dependencies": {
"moment": "^2.23.0"
},
"devDependencies": {
"webpack": "^4.28.4",
"webpack-cli": "^3.2.1"
}
}
Listing 9: package.json - NPM-Abhängigkeiten definieren

Webpack als Gradle-Task

Im nächsten Schritt wird in der build.gradle ein neuer Task mit dem Namen webpack definiert. Als Typ wird dabei der NodeTask aus dem Node-Plug-in benutzt. Dieser Typ ermöglicht die Ausführung von Node.js-Skripten als Teil des Build-Prozesses. Da mit diesem Task der JavaScript-Quellcode zusammengepackt werden soll, muss webpack als ausführbares Skript angegeben werden. Dieses befindet sich in dem von NPM automatisch angelegten node_modules/.bin-Verzeichnis. In diesem Ordner legt Node.js alle ausführbaren Skripte nach ihrer Installation ab. Damit webpack die Einstiegsdatei finden und das Ergebnis am richtigen Ort wieder ablegen kann, müssen entsprechende Argumente an das auszuführende Skript übergeben werden. Das erste Argument ist dabei der Pfad zu jener JavaScript-Datei, die als Einstiegspunkt dient und alle anderen Dateien importiert. Im Falle dieser Entwicklung ist das die index.js. Da Webpack theoretisch auch mehrere Quellen verarbeiten kann, wird als zweites Argument der -o-Parameter (-o für Output) angegeben, um die Angabe der Zieldatei festzulegen. Als drittes Argument wird dementsprechend der Pfad zur Zieldatei angegeben. Damit dieser Task vom Gradle-Cache profitieren kann und nur dann erneut ausgeführt wird, wenn sich am Code etwas geändert hat oder die Zieldatei gelöscht wurde, müssen die entsprechenden Verzeichnisse unter Beobachtung gestellt werden. Mit inputs.dir und outputs.dir können die entsprechenden Pfade festgelegt werden. Listing 10 zeigt die vollständige Implementierung des Gradle-Tasks.

task webpack(type: NodeTask) {
inputs.dir("src/main/webapp/js")
outputs.dir("src/main/webapp/dist/js")
script = file("$projectDir/node_modules/.bin/webpack")
args = [
"src/main/webapp/js/index.js", "-o",
"src/main/webapp/dist/js/main.js"
]
}
Listing 10: gradle.build – den Gradle-Task webpack implementieren

Nun wurde dafür gesorgt, dass das nötige JavaScript durch den Build-Prozess gepackt und über die Einbindung in der BasePage in der Webanwendung geladen werden kann. Auch wenn der neue HTML-Tag time-gap jetzt schon benutzt werden könnte, fehlt hier immer noch ein wichtiger Schritt der Integration – die Datenbindung.

Datenbindung

Die Datenbindung ist stark davon abhängig, welches Webframework eingesetzt wird. In Wicket kann beispielsweise die Kombination aus einem AttributeAppender und einem Model verwendet werden, um einen aus der Datenbank geladenen oder errechneten Wert an ein bestimmtes Attribut eines Tags im HTML zu binden. Listings 11 und 12 und zeigen, wie eine solche Datenbindung für die TimeGap in der Webanwendung Tasks aussehen könnte.

<time-gap wicket:id="created" locale="de"></time-gap>
Listing 11: TaskListItemPanel.html – Time-Gap im HTML einbinden
//...
private void addTimeGap(IModel<LocalDateTime> createdModel) {
WebMarkupContainer timeGap =
new WebMarkupContainer("created");
timeGap.add(new AttributeAppender("from", createdModel));
add(timeGap);
}
//...
Listing 12: TaskListItemPanel.java – Datenbindung mit AttributeAppender

Ein anderer Weg, der eine solche Komponente im Wicket-Kontext wiederverwendbarer macht, ist die Implementierung einer eigenen Component-Klasse. In dieser speziellen Component-Klasse kann zum einen der richtige Tag-Name gesetzt, zum anderen die Datenbindung an die entsprechenden Attribute gekapselt werden. Der Aufrufer dieser Klasse muss dann nur noch ein passendes Model vom Typ LocalDateTime an den Konstruktor übergeben, um den Rest kümmert sich die Klasse selbst. In Listing 13 ist eine mögliche Implementierung und Listings 14 und 15 ihre Verwendung abgebildet.

public class TimeGap extends
GenericWebMarkupContainer<LocalDateTime> {
public TimeGap(String id, IModel<LocalDateTime> model) {
super(id, model);
}
@Override
protected void onComponentTag(ComponentTag tag) {
super.onComponentTag(tag);
tag.setName("time-gap");
tag.put("from", getModelObject().toString());
tag.put("locale", getLocale().getCountry());
}
}
Listing 13: TimeGap.java – TimeGap als Wicket-Komponente implementieren
//...
add(new TimeGap("created", createdModel));
//...
Listing 14: TaskListItemPanel.java – Die Wicket-Komponente TimeGap verwenden
<!-- ... -->
<span wicket:id="created"></span>
<!-- ... -->
Listing 15: TaskListItemPanel.html – TimeGap im HTML einsetzen

Abbildung 3 stellt die angepasste Benutzeroberfläche der Webanwendung dar. An Stelle von Datums- und Zeitwerten erscheinen nun entsprechende Zeitabstände, die durch die eingebundene WebComponent errechnet und in der gewünschten Sprache zur Anzeige gebracht werden.

Abbildung 3: Der Einsatz von TimeGap

Fazit

In diesem Artikel wurde gezeigt, wie einfach sich eine Web Component implementieren und in ein bestehendes Gradle-Projekt mit einem Java-basierten Server-Side-Rendering-Webframework integrieren lässt. Das Konzept der Kapselung von Mark-up und Logik bei WebComponents erhöht nicht nur ihren Wiederverwendungswert, auch die Wartbarkeit wird dadurch enorm erleichtert. Da sich solche Komponenten wie native HTML-Elemente verhalten, müssen ihre Anwender zum einen keinen Initialisierungscode schreiben und können zum anderen von der einfachen Datenbindung über Tag-Attribute profitieren. Die vorgestellte Node.js-Integration liefert den Vorteil, dass die eingesetzten JavaScript-Bibliotheken als NPM-Pakete verwaltet und damit nicht in das Quellcode-Repository eingecheckt werden müssen. Zusätzlich eröffnet sie die Möglichkeit, einen vollständigen JavaScript-Build mit einem dedizierten Task-Runner, wie beispielsweise Gulp, in den Build-Prozess der Webanwendung einbeziehen zu können.

Zur Blog-Post Übersicht