Vaadin-Widgets mit JavaScript erweitern

Vaadin und JavaScript

Wajdi Almir Ahmad

© Shutterstock.com / BGSmith

Mit seinem rein Java-basierten, serverseitigen Ansatz für die Webentwicklung gewann das Vaadin-Framework in den letzten Jahren immer mehr an Zuspruch. Seit der siebten Version des Frameworks wurde die Erweiterbarkeit wesentlich verbessert und die Entwicklung von eigenen UI-Komponenten vereinfacht. Dies erleichtert die Integration von JavaScript-Bibliotheken in den eigenen Widgets und bereichert die Möglichkeiten der Webentwicklung mit Vaadin.

Der größte Teil der Entwicklung mit Vaadin geschieht auf der Serverseite, bei der die Entwickler Webapplikationen mit Vaadins eventbasiertem Ansatz in einem Java-Swing-ähnlichen Stil entwerfen und steuern können. Auf der Clientseite baut Vaadin auf dem Google Web Toolkit (GWT) auf und abstrahiert damit von clientseitigen Technologien und Browserinkompatibilitätsproblemen. Schließlich fügt Vaadin, transparent für den Entwickler, eine Kommunikationsebene zwischen der Server- und Clientseite hinzu. Dementsprechend erfordert die Implementierung eigene Vaadin-UI-Komponenten: erstens die Erstellung des clientseitigen GWT-basierten Teils, zweitens die serverseitige Steuerkomponente und drittens die Definition und Implementierung der Kommunikationsschnittstellen zwischen den beiden Seiten.

Browserinkompatibilitätsproblemen. Schließlich fügt Vaadin, transparent für den Entwickler, eine Kommunikationsebene zwischen der Server- und Clientseite hinzu. Dementsprechend erfordert die Implementierung eigene Vaadin-UI-Komponenten: erstens die Erstellung des clientseitigen GWT-basierten Teils, zweitens die serverseitige Steuerkomponente und drittens die Definition und Implementierung der Kommunikationsschnittstellen zwischen den beiden Seiten.

Um diesen Prozess zu veranschaulichen, wird in diesem Artikel das herkömmliche Vaadin-Textfeld mit dem jQuery-Plug-in jquery.inputmask erweitert, mit dem Ziel, die Funktionalitäten des Plug-ins serverseitig steuerbar zu machen, sowie vom Plug-in gefeuerte Events an den Server zurück zu übertragen.

jquery.inputmask-Plug-in

jquery.inputmask ist ein jQuery-Plug-in für die Erstellung von Eingabemasken, durch welches Entwickler die möglichen Eingabezeichen eines Felds beliebig einschränken können. Zusätzlich können mit diesem Plug-in Trennzeichen und Platzhalter festgelegt werden. Das Plug-in bietet per Default nur drei Maskendefinitionen:

  • 9: numerisch
  • a: alphabetisch
  • *: alphanumerisch

Allerdings können diese Definitionen durch mehrere bereitgestellte Extensions erweitert werden, beispielsweise für die Definition eines Datums oder einer Telefonnummer. Außerdem können eigene Maskendefinitionen leicht konfiguriert werden.

Im folgenden Beispiel wird die Eingabemöglichkeit im Feld test auf drei Ziffern, ein Strich als Trennzeichen, gefolgt von drei Buchstaben, beschränkt. Die eckigen Klammern geben lediglich an, dass ein Teil der Maske optional ist:

$('#test').inputmask('999[-aaa]');

Versucht ein Benutzer einen Buchstaben bei den ersten drei Zeichen einzutippen oder eine Zahl bei den letzten drei Zeichen, wird seine Eingabe ignoriert.

Zusätzliche Parameter, wie z. B. Platzhalter oder Callback-Methoden zum Event Handling, können in einem Objekt an die Methode inputmask() übergeben werden (Listing 1).

Listing 1
$('#test').inputmask('999', 
  {"placeholder": "###", 
    "oncomplete": function(){ alert('inputmask complete');
});

Die zu implementierende UI-Komponente dagegen sieht ein weniger fehleranfälliges Java-API vor, mit separaten Methoden zum Setzen der Maske und dem Platzhalter sowie zur Registrierung von Event-Listenern. Das Beispiel aus Listing 1 würde dann mit dem neuen Java-API wie in Listing 2 aussehen.

Listing 2
testField.setMask('999');
testField.setPlaceholder('###');
testField.addOnCompleteEventListener( 
  () -> Notifications.show('inputmask complete') );

Widgets, Widget-Sets und Vaadin

Vaadin liefert eine ausführliche Menge an UI-Komponenten, inklusive Slidern, interaktiven Tabellen und sogar ein Farbauswahlelement. Die Sammlung aller UI-Komponenten, die ein Entwickler in einer Vaadin-Applikation verwenden kann, wird als Widget-Set bezeichnet. Der Begriff Widget an sich stammt jedoch aus der Google-Web-Toolkit-Welt und beschreibt ein einzelnes UI-Element, das zu JavaScript und HTML kompiliert und im Browser gerendert wird.

Standardgemäß verwendet eine Vaadin-Applikation das so genannte Default-Widget-Set, das wiederum durch eine Vielzahl an Vaadin-Add-ons aus dem Vaadin Add-ons Directory erweitert werden kann. Dennoch gibt es Fälle, bei denen diese Auswahl nicht ausreichend ist. Für diese Fälle bietet es sich an, eigene Vaadin-Widgets zu implementieren.

Solch ein Vorhaben ist zwar verbunden mit einem erhöhten Aufwand in der Anfangsphase der Entwicklung, macht sich jedoch durch die gewonnene Flexibilität und den vereinfachten Zugriff auf clientseitige Ressourcen bezahlt.

Deferred Binding in GWT

Um Webentwicklern den Ärger um Browserkompatibilität zu ersparen, setzt GWT auf ein Feature namens Deferred Binding. Durch dieses Feature ist das Framework in der Lage, mehrere Varianten einer Klasse während des Kompilierens zu erstellen und anschließend, während der Laufzeit, die richtige an den Benutzer zu liefern. Die Generierung der einzelnen Varianten ist meistens browserabhängig. Weitere Umgebungsparameter, wie z. B. die Sprache, können ebenfalls einbezogen werden. In diesem Fall wird eine Variante pro Browser und Sprache bereitgestellt.

An vielen Stellen im Toolkit wird Deferred Binding implizit verwendet. Bei der Entwicklung eigener Widgets wird jedoch bei manchen Klassen die explizite Verwendung notwendig. Dabei wird zur Instanziierung dieser Klassen anstatt des new-Operators die Methode GWT.create(Class) verwendet. Dies erlaubt GWT an einem späteren Zeitpunkt zu entscheiden, welche Version der Klasse zurückgegeben wird.

Die Bestandteile einer UI-Komponente

Abbildung 1 zeigt die einzelnen Komponenten eines Vaadin-UI-Elements auf. MyWidget ist eine eigenentwickelte clientseitige Komponente, die für die Darstellung im Browser bestimmt ist. Ihr serverseitiges Gegenstück ist MyComponent, in dem das API implementiert wird, das es Entwicklern erlaubt, das Widget serverseitig zu steuern. Die beiden Hälften teilen ein gemeinsames Zustandsobjekt, MyComponentState, und kommunizieren über den MyConnector miteinander, welcher die Aufgabe hat, die Synchronisation des Zustandsobjekts sowie die Events vom und zum Server zu verwalten.

Initiales Setup

Der einfachste Weg, mit der Entwicklung von Vaadin-Widgets loszulegen, ist die Verwendung des Vaadin-Plug-ins für Ihre IDE. Das Plug-in ist mittlerweile für Eclipse, NetBeans und neuerdings auch für IntelliJ IDEA verfügbar. Dadurch lassen sich mithilfe des grafischen Wizards Templates für Vaadin-Projekte, Themes oder Widgets anlegen.

Um nicht an eine IDE gebunden zu sein und deshalb, weil in der Praxis Projekte selten ohne die Verwendung eines Build-Tools entwickelt werden, wird in diesem Beitrag das Widget als ein Maven-Projekt implementiert, unter Verwendung des Vaadin-Archetype-Widget (Tabelle 1).

Tabelle 1: Das Maven-Archetype-Vaadin-Archetype-Widget

Tabelle 1: Das Maven-Archetype-Vaadin-Archetype-Widget

Die Generierung, basierend auf diesem Archetype, wird mit dem Maven-Befehl mvn archetype:generate gestartet, und es werden folgende Parameterwerte übergeben:

  • groupId: de.adesso.jm
  • artifactId: maskedinputfield
  • version: 1.0-SNAPSHOT
  • package: de.adesso.jm.maskedinput
  • ComponentClassName: MaskedInputField

Damit wird ein Maven-Projekt mit zwei Modulen generiert: maskedinputfield und maskedinputfield-demo. Im Demomodul befindet sich eine beispielhafte Vaadin-Applikation, in der das neue Widget getestet werden kann. Zusätzlich enthält die POM-Datei dieses Moduls die Deklaration des Vaadin-Maven-Plug-ins. Dieses Plug-in übernimmt die Kompilierung des Widgets während des Maven-Build-Prozesses (Listing 3).

Listing 3
<plugin>
  <groupId>com.vaadin</groupId>
  <artifactId>vaadin-maven-plugin</artifactId>
  <version>${vaadin.plugin.version}</version>
  <configuration>
    <webappDirectory>
      ${basedir}/src/main/webapp/VAADIN/widgetsets
    </webappDirectory>
    <hostedWebapp>
      ${basedir}/src/main/webapp/VAADIN/widgetsets
    </hostedWebapp>
    ...
  </configuration>
  <executions>
    <execution>
      ...
      <goals>
        <goal>resources</goal>
        <goal>update-widgetset</goal>
        <goal>compile</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Das Goal update-widgetset ist für die Neukompilierung des Widget-Sets verantwortlich und kann einzeln aus der Kommandozeile mit dem Befehl mvn vaadin:update-widgetset ausgeführt werden.

Im Modul maskedinputfield dagegen sind die Package-Struktur sowie alle notwendigen Klassen generiert worden, die für die Implementierung der in Abbildung 1 dargestellten Komponenten des Widgets notwendig sind (Abb. 2). Zusätzlich wird im Ressourcenverzeichnis ein GWT-Modul Descriptor (die Datei WidgetSet.gwt.xml) angelegt, der den Eingangspunkt der clientseitigen Engine darstellt.

Abschließend müssen die Quelldateien für jquery.inputmask und jQuery heruntergeladen und im Ressourcenverzeichnis unter dem Pfad de/adesso/jm/maskedinputfield/client abgelegt werden.

Abb. 2: Package-Struktur des Moduls „MaskedInputField“

Abb. 2: Package-Struktur des Moduls „MaskedInputField“

Die Clientseite

Im Package de.adesso.jm.maskedinputfield.client des Moduls MaskedInputField sind sämtliche Klassen, die für die Implementierung des Widgets sowie deren Integration mit der Serverseite relevant sind, generiert worden. Dieser Abschnitt bietet einen detaillierteren Einblick in die einzelnen Klassen sowie die durchzuführenden Anpassungen, angefangen mit der Klasse MaskedInputFieldWidget.

Wie bereits erwähnt, deutet das Suffix Widget im Namen der Klasse darauf hin, dass diese Klasse die (GWT-basierte) Clientseite repräsentiert. Da im Grunde das Ziel ist, das Eingabefeld nur zu erweitern, bietet es sich an, diese Implementierung von der Klasse des GWT-Widgets com.google.gwt.user.client.ui.TextBox erben zu lassen. Zusätzlich implementiert diese Klasse das Interface com.vaadin.client.ui.Field, das als ein so genanntes Markierungsinterface keine Methoden vorschreibt, sondern lediglich dazu dient, Vaadin serverseitig mitzuteilen, dass es sich bei diesem Widget um ein Eingabefeld handelt.

Die Hauptaufgabe der Klasse MaskedInputFieldWidget ist es, die Werte der Platzhalter und Maske von der serverseitigen Hälfte an dem JavaScript-Plug-in clientseitig weiterzuleiten. Dafür werden die beiden Methoden setPlaceholder() und setMask() benötigt. Außerdem wird zum Event Handling ein einfaches Observer-Pattern implementiert. Dies besteht aus einem internen Handler-Interface, MaskEventHandler, sowie den dazugehörigen Methoden, um neue Handler zu registrieren und auf Events zu reagieren sowie schließlich zwei Handler-Listen. Listing 4 zeigt nun den dafür notwendigen Code und die vorerst leeren setMask() und setPlaceholder()-Methoden.

Als ein Vaadin „Best-Practice“ wird zusätzlich ein CSS-Klassenname für das Widget definiert und im Konstruktor an die Methode addStyleName() übergeben. Dies kann später die Arbeit der Designer vereinfachen.

Listing 4
public class MaskedInputFieldWidget extends TextBox implements Field {
  
  public static final String CLASSNAME = "ad-masked-input-field";

  private String mask;
  private String placeholder;
  
  private List maskCompleteHandlers;
  private List maskIncompleteHandlers;
  
  public MaskedInputFieldWidget() {
    setStyleName(CLASSNAME);
    maskCompleteHandlers = new ArrayList<>();
    maskIncompleteHandlers = new ArrayList<>();
    ...
  }
  
  public void setInputMask(String mask){
    ...
  }
  
  public void setPlaceholder(String placeholder){
    ...
  }
  
  public void addOnMaskCompleteHandler(MaskEventHandler handler) {
    maskCompleteHandlers.add(handler);
  }

  public void addOnMaskIncompleteHandler(MaskEventHandler handler) {
    maskIncompleteHandlers.add(handler);
  }

  private void onMaskComplete() {
    for (MaskEventHandler handler : maskCompleteHandlers){
      handler.handleMaskEvent();
    }
  }

  private void onMaskIncomplete() {
    for (MaskEventHandler handler : maskIncompleteHandlers) {
      handler.handleMaskEvent();
    }
  }
  
  protected interface MaskEventHandler {
    void handleMaskEvent();
  } 
}   

Geteilter Zustand

Wie in Abbildung 1 dargestellt, werden Zustandsinformationen einer UI-Komponente über ein Shared-State-Objekt vom Server zum Client geteilt. Wird zum Beispiel die Beschriftung eines Felds serverseitig geändert, entnimmt die Clienthälfte den neuen Wert aus dem Zustandsobjekt und aktualisiert ihn anschließend im Browser.

Dementsprechend muss die Zustandsklasse des neuen Widgets (MaskedInputFieldState) um die zwei neuen Zustandswerte mask und placeholder erweitert werden. Da ein Zustandsobjekt keine Logik enthalten darf, reicht es, die beiden neuen Parameter als public zu deklarieren. Die Definition von Bean-Parametern mit Getter und Setter ist jedoch ebenfalls möglich.

Alle anderen für ein Textfeld relevanten Zustandswerte, wie z. B. die maximale Länge oder gar der Textinhalt des Felds, werden dadurch verfügbar gemacht, dass die Klasse MaskedInputFieldState von com.vaadin.shared.ui.textfield.AbstractTextFieldState erbt (Listing 5), welche wiederum von der Oberklasse aller Zustandsobjekte com.vaadin.shared.communication.SharedState erbt.

Listing 5
public class MaskedInputFieldState 
  extends AbstractTextFieldState {
  
  public String mask;
  public String placeholder;
}

Die Verbindung zwischen Server und Client

Für den Informationsaustausch zwischen Server und Client ist der Connector zuständig. Ein Connector ist eine clientseitige Klasse, die die Aufgabe hat, serverseitige Änderungen im Zustandsobjekt an das Widget zu kommunizieren, sowie Userinteraktionen an die Serverseite weiterzureichen.

Um die Widget-Klasse mit ihrem serverseitigen Gegenstück zu verbinden, wird die Klasse MaskedInputFieldConnector folgendermaßen erweitert: Als Erstes werden die Methoden getWidget() und getState() der Oberklasse AbstractComponentConnector überschrieben, um einen Cast zum richtigen Klassentyp durchführen zu können. Dies ermöglicht dem Connector den Zugriff auf die neuen Setter-Methoden des Widgets sowie auf die neuen Zustandsparameter des Shared States. Um das Widget mit dem richtigen Klassentyp erstellen zu können, muss zusätzlich die Methode createWidget() überschrieben werden. Dabei ist zu beachten, dass die Instanziierung des Widgets nicht mit dem new-Operator durchgeführt werden darf, sondern durch „Deferred Binding“ mit der Methode GWT.create(). Anschließend wird über die Annotation @Connect die serverseitige Klasse der Komponente festgesetzt. Zuletzt wird die Methode onStateChanged() überschrieben, um bei Zustandsänderungen die Werte aus dem Zustandsobjekt zu lesen und das Widget zu aktualisieren (Listing 6).

Listing 6
@Connect(MaskedInputField.class)
public class MaskedInputFieldConnector 
  extends AbstractComponentConnector {

  @Override
  protected Widget createWidget() {
    return GWT.create(MaskedInputFieldWidget.class);
  }

  @Override
  public MaskedInputFieldWidget getWidget() {
    return (MaskedInputFieldWidget) super.getWidget();
  }

  @Override
  public MaskedInputFieldState getState() {
    return (MaskedInputFieldState) super.getState();
  }

  @Override
  public void onStateChanged(StateChangeEvent stateChangeEvent) {
    super.onStateChanged(stateChangeEvent);
    String mask = getState().mask;
    String placehoder = getState().placeholder;
    getWidget().setInputMask(mask);
    getWidget().setPlaceholder(placehoder);
  }
}

Um Synchronisationsprobleme beim Shared State auszuschließen, ist die Änderung der Zustandsparameter nur in einer Richtung möglich, nämlich die vom Server zum Client. Clientseitige Änderungsversuche werden ignoriert und beim nächsten serverseitigen Zugriff einfach überschrieben.

Um jedoch dem clientseitigen Widget die Änderung der Zustandswerte zu erlauben, ohne die Konsistenz des Shared States zu gefährden, wird auf das Pattern Remote Procedure Call (RPC) zurückgegriffen. Die beiden Interfaces MaskedInputServerRpc und MaskedInputClientRpc definieren die Methoden, die über das RPC-Pattern aufrufbar sind und erlauben damit server- bzw. clientseitigen Komponenten, Methoden auf der jeweils anderen Seite auszuführen.

Für das neue Widget werden im ServerRpc-Interface Methoden definiert, um serverseitig Events zu feuern, die die Vollständigkeit bzw. Unvollständigkeit des Felds melden (Listing 7). Das ClientRPCInterface wird für dieses Beispiel nicht gebraucht. Letztlich werden die RPC-Methoden im Konstruktor der Connector-Klasse aufgerufen (Listing 8). Die Methode getRpcProxy() liefert ein RPC-Proxy-Objekt zurück, das das Aufrufen der serverseitigen Methoden ermöglicht.

Listing 7
public interface MaskedInputFieldServerRpc extends ServerRpc {
  void fireMaskCompleteEvent();
  void fireMaskIncompleteEvent();
}
Listing 8
public class MaskedInputFieldConnector extends AbstractComponentConnector {

  public MaskedInputFieldConnector() {
 
    getWidget().addOnMaskIncompleteHandler(new MaskEventHandler() {
      @Override
      public void handleMask() {
        getRpcProxy(MaskedInputFieldServerRpc.class).fireMaskIncompleteEvent();
      }
    });
    getWidget().addOnMaskCompleteHandler(new MaskEventHandler() {
      @Override
      public void handleMask() {
        getRpcProxy(MaskedInputFieldServerRpc.class).fireMaskCompleteEvent();
      }
    });  }
  ...
}

Die Serverseite

Serverseitig besteht die UI-Komponente aus der Klasse MaskedInputField, die analog zu ihrem clientseitigen Gegenstück von der Serverkomponente des Textfelds die Klasse com.vaadin.ui.TextField erbt.

Um nun die Steuerung des Widgets serverseitig zu ermöglichen, muss die Klasse MaskedInputField auf folgende Weise angepasst werden: Zunächst muss die Methode getState() überschrieben werden, um erneut einen Cast zum richtigen Klassentyp durchzuführen. Dadurch ist es möglich, in den zwei neuen Methoden setMask() und setPlaceholder() auf die Zustandsparameter zuzugreifen und ihnen neue Werte zuweisen zu können (Listing 9). Clientseitig reagiert der Connector auf die Änderungen im Shared State und leitet anschließend die neuen Werte an das Widget weiter. Das Setzen einer ID im Konstruktor ermöglicht die eindeutige Identifikation des Textfelds im Browser.

Listing 9
public class MaskedInputField extends TextField {
  public MaskedInputField() {
    setId(UUID.randomUUID().toString());
    ...
  }
  ...
  
  @Override
  protected MaskedInputFieldState getState() {
    return (MaskedInputFieldState) super.getState();
  }

  public void setMask(String mask){
    getState().mask = mask;
  }

  public void setPlaceholder(String placeholder) {
    getState().placeholder = placeholder;
  }
}

Somit ist es nun möglich, das Widget serverseitig zu steuern. Um allerdings auf die vom jQuery-Plug-in gefeuerten Events serverseitig zu reagieren, fehlt der Serverkomponente eine Implementierung des Server-RPC-Interface. Der Einfachheit halber wird das Interface als eine anonyme Klasse im Konstruktor der Serverkomponente implementiert und anschließend über die Methode registerRpc() registriert. Durch die Registrierung werden die vom Client stammenden Methodenaufrufe mit den richtigen serverseitigen Methodenimplementierungen verbunden.

Analog zum clientseitigen Widget wird für das Management von Events und Event-Listeners wieder auf das Observer-Pattern zurückgegriffen. Dazu gehören die zwei internen Listener-Interfaces, MaskCompleteListener und MaskIncompleteListener, Methoden zur Registrierung neuer Listener sowie zum Event-Feuern und schließlich zwei Listen, um die Listener zu verwalten (Listing 10).

Listing 10
public class MaskedInputField extends TextField {
  private List maskCompleteListeners;
  private List maskIncompleteListeners;

  public MaskedInputField() {
    setId(UUID.randomUUID().toString());
    maskCompleteListeners = new ArrayList<>();
    maskIncompleteListeners = new ArrayList<>();
    
    registerRpc(new MaskedInputFieldServerRpc() {
      @Override
      public void updateValue(String newValue) {
        setValue(newValue);
      }
      @Override
      public void fireMaskCompleteEvent() {
        MaskedInputField.this.fireMaskCompleteEvent();
      }
      @Override
      public void fireMaskIncompleteEvent() {
        MaskedInputField.this.fireMaskIncompleteEvent();
      }
    });
  }
  
  private void fireMaskCompleteEvent() {
    for (MaskCompleteListener l : maskCompleteListeners) {
      l.onMaskComplete();
    }
  }

  private void fireMaskIncompleteEvent() {
    for (MaskIncompleteListener l : maskIncompleteListeners) {
      l.onMaskIncomplete();
    }
  }
  
  public interface MaskCompleteListener {
    void onMaskComplete();
  }

  public interface MaskIncompleteListener {
    void onMaskIncomplete();
  }  
  ...
}

GWTs JavaScript Native Interface

Das letzte Stück des Puzzles ist die Integration der JavaScript-Bibliothek jquery.inputmask. Bis zu diesem Stand werden die Werte der Maske und Platzhalter von der Serverseite über den Connector an das Widget weitergeleitet. Um diese nun an die passenden JavaScript-Funktionen zu übergeben, wird auf ein GWT-Feature namens JavaScript Native Interface (JSNI) zurückgegriffen.

JSNI greift Konzepte aus dem Java API Java Native Interface (JNI), das es Entwicklern erlaubt, Methoden aus plattformabhängigen Applikationen und Bibliotheken aufzurufen. Diese so genannten „nativen“ Applikationen können in einer beliebigen anderen Sprache geschrieben werden, wie z. B. C oder Assembly. Für die JSNI-Variante ist die Plattform der Browser und der Code ist in JavaScript geschrieben.

Im Java-Code müssen Methoden die „nativen“ Codes aufrufen. Diese sollen mit dem Keyword native gekennzeichnet werden. Weiterhin erlaubt JSNI dem JavaScript-Code, statt in externe Ressourcen direkt im Methodenrumpf zu schreiben. Dazu müssen die Rümpfe in einen speziellen JSNI-Comment-Block gesetzt werden. Der Block fängt mit /*-{ an und endet mit }-*/ (Listing 11).

Listing 11
public static native void alert(String msg) /*-{
  $wnd.alert(msg);
}-*/;

Bevor die nativen Methoden definiert werden können, müssen die nötigen JavaScript-Ressourcen geladen werden. In GWT wird das durch die Definition eines ClientBundle-Interface ermöglicht. Dementsprechend wird das Interface MaskedInputFieldResources, das von com.google.gwt.resources.client.ClientBundle erbt, erstellt. Dieses Interface definiert die zwei Methoden jquery() und inputmask(), die JavaScript-Skripte als Objekte der Klasse TextResource zurückliefern. Das Binding zwischen den jeweiligen Methoden und den eigentlichen Dateien, die sich im Ressourcenverzeichnis des Moduls befinden, wird über die Annotation @Source erreicht. Schließlich wird die Instanziierung eines Objekts dieses Interface GWT durch die Methode create() überlassen und als statische Konstante namens INSTANCE abgelegt (Listing 12).

Listing 12
@Connect(MaskedInputField.class)
public class MaskedInputFieldConnector 
  extends AbstractComponentConnector {

  public MaskedInputFieldConnector() {
    getWidget().addOnMaskIncompleteHandler(new MaskEventHandler() {
      @Override
      public void handleMaskEvent() {
        getRpcProxy(MaskedInputFieldServerRpc.class).fireMaskIncompleteEvent();
      }
    });

    getWidget().addOnMaskCompleteHandler(new MaskEventHandler() {
      @Override
      public void handleMaskEvent() {
        getRpcProxy(MaskedInputFieldServerRpc.class).fireMaskCompleteEvent();
      }
    });  
  }
  ...
}

Zusätzlich wird die Hilfsklasse MaskedInputFieldScriptLoader erstellt, die die Aufgabe hat, die JavaScript-Ressourcen in der Webapplikationen zu injizieren. Dies geschieht in der nativen Methode inject(), indem die JavaScript-eval()-Methode aufgerufen wird (Listing 13). Wichtig ist dabei zu beachten, dass das Objekt window nicht direkt referenziert werden kann, da das kompilierte Skript schließlich in einem Frame auf der „Hostseite“ läuft. Stattdessen kann die Browservariable window sowie auch document nur über $wnd bzw. $doc referenziert werden. Diese Parameter werden von GWT so initialisiert, um die window– und document-Objekte der Hostseite zurückzuliefern und nicht die des Frames.

Der restliche Code dient lediglich dazu, die Ressourcen aus der statischen Instanz des ClientBundle-Interface zu holen und sie bei Bedarf über die inject()Methode zu laden. Auch hier wird die Klasse MaskedInputFieldScriptLoader über GWTs Deferred-Binding-Prinzip initialisiert und in einer eigenen INSTANCE-Konstante abgelegt (Listing 13).

Listing 13
public class MaskedInputFieldScriptLoader {

  public static final MaskedInputFieldScriptLoader INSTANCE = GWT
      .create(MaskedInputFieldScriptLoader.class);

  private static boolean injected = false;

  public static void ensureInjectedResources() {
    if (!injected) {
      INSTANCE.injectResources();
      injected = true;
    }
  }

  protected void injectResources() {
      if (!hasJQuery()) {
        inject(MaskedInputFieldResources.INSTANCE.jquery().getText());
      }
      inject(MaskedInputFieldResources.INSTANCE.inputmask().getText());
  }

  protected static native boolean hasJQuery()
  /*-{
      if($wnd.jQuery){
          return true;
      }
      return false;
  }-*/;

  protected static native void inject(String script)
  /*-{
      $wnd.eval(script);
  }-*/;

}

Dieser Ansatz, in dem das Client-Bundle in Kombination mit einer Script-Loader-Klasse verwendet wird, ist vom Code des vom Vaadin-Team entwickelten Charts-Add-on inspiriert worden.

Nachdem nun die JavaScript-Ressourcen eingebunden worden sind, kann die Widget-Klasse MaskedInputWidget vervollständigt werden. Zuerst wird im Konstruktor die statische Methode ensureResourcesInjection() aus dem Script Loader aufgerufen. Anschließend werden die beiden Methoden setMask() und setPlaceholder() erweitert, um die neuen Werte an die native Methode setFieldProperties() zu überreichen.

Das jquery.inputmask-Plug-in erlaubt es, über ein Parameterobjekt JavaScript-Methoden zu definieren, die auf bestimmte Events reagieren. Um aber eine Java-Methode benutzen zu können, wird auf ein weiteres Feature von JSNI zurückgegriffen. Der Methodenaufruf sieht folgendermaßen aus:

[instance-expr.]@class-name::method-name(param-signature)(arguments)

In der Methode setFieldProperties() wird ein Objekt props erstellt, das die beiden Callback-Methoden sowie gegebenenfalls den Wert des Platzhalters festlegt und schließlich an die JavaScript-Methode inputmask() übergeben wird. Die finalen Erweiterungen der Klasse MaskedInputWidget sind in Listing 14 dargestellt.

Listing 14
public class MaskedInputFieldWidget extends TextBox implements Field {
  ...

  public void setInputMask(String mask) {
    if (mask != null && !mask.isEmpty() 
        && !mask.equals(this.mask)){
      this.mask = mask;
      setFieldProperties(this.getElement().getId(), 
        this.mask, this.placeholder);
    }
  }

  public void setPlaceholder(String placeholder) {
    if (placeholder != null && !placeholder.isEmpty()
        && !placeholder.equals(this.placeholder)) {
      this.placeholder = placeholder;
      setFieldProperties(this.getElement().getId(), 
        this.mask, this.placeholder);
    }
  }
  ...
  private native void setFieldProperties(String elementId,
      String mask, String placeholder)
  /*-{
     var thisWidget = this;
     var props = { 
       oncomplete : function(){
         thisWidget.@de.adesso.jm.maskedinputfield.client
           .MaskedInputFieldWidget::onMaskComplete()()},
       onincomplete : function(){
         thisWidget.@de.adesso.jm.maskedinputfield.client
           .MaskedInputFieldWidget::onMaskIncomplete()()}
     };
     if (placeholder !== ""){
       props.placeholder = placeholder;
     }
     $wnd.jQuery('#' + elementId).inputmask(mask, props);
  }-*/;

}

Somit ist die Implementierung des neuen Widgets abgeschlossen, und es ist nun möglich, eine Demo durchzuführen.

Demo

Im generierten Demomodul muss in der Klasse UI-Klasse DemoUI die Methode init(), die den Eingangspunkt einer Vaadin-Applikation darstellt, bearbeitet werden.

In der Demoapplikation wird eine neue MaskedInputField-Komponente abgelegt, mit der Maske „999-aaa“ und dem Platzhalter „###-???“. Zusätzlich werden zwei einfache Event-Listener (mittels Lambda-Ausdrücken) definiert, die lediglich darüber benachrichtigen, ob die Maske vollständig ist oder nicht (Listing 15).

Listing 15
protected void init(VaadinRequest request) {
  final MaskedInputField component = new MaskedInputField();

  component.setMask("999[-aaa]");
  component.setPlaceholder("###-???");
  component.addMaskCompleteListener(() -> Notification.show("complete"));
  component.addMaskIncompleteListener(() -> Notification.show("incomplete"));

  final VerticalLayout layout = new VerticalLayout();
  layout.setStyleName("demoContentLayout");
  layout.setSizeFull();
  layout.addComponent(component);
  layout.setComponentAlignment(component, Alignment.TOP_CENTER);
  setContent(layout);
}

Die Applikation kann nun mit Maven gebaut und in einen beliebigen Servlet-Container deployt und gestartet werden. Das Ergebnis ist in den Abbildungen 3 und 4 dargestellt.

Abb. 3: Demo-Vaadin-Applikation (Feld leer)

Abb. 3: Demo-Vaadin-Applikation (Feld leer)

Abb. 4: Demo-Vaadin-Applikation (Feld vollständig)

Abb. 4: Demo-Vaadin-Applikation (Feld vollständig)

Fazit

Der Ansatz des Vaadin-Frameworks verfolgt eine serverseitige, rein Java-basierte Webentwicklung ohne direkte Verwendung von HTML und JavaScript-Code, schließt diese jedoch nicht aus. Durch die verbesserte Erweiterbarkeit des Frameworks können selbstentwickelte Add-ons auf verschiedenen Abstraktionsebenen entwickelt werden – wie zum Beispiel rein serverseitig, clientseitig mit HTML und JavaScript oder, wie das Beispiel aus diesem Artikel, clientseitig auf GWT-Ebene.

Weiterhin zeigt dieser Artikel auf, wie solche Add-ons die Vorteile des Programmierens mit einer typisierten Sprache wie Java mit der Flexibilität und Vielfalt anderer Webtechnologien kombinieren können.

Aufmacherbild: Wild Antlered bull elk via Shutterstock / Urheberrecht: BGSmith

Verwandte Themen:

Geschrieben von
Wajdi Almir Ahmad
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Vaadin und JavaScript"

avatar
4000
  Subscribe  
Benachrichtige mich zu:
trackback

[…] Almir Ahmad has written a nice tutorial to jaxenter.de that covers using JS libraries with GWT JSNI binding and Vaadin. Although in Vaadin 7 there is also […]

Victor Röder
Gast

Hallo,

ich befürchte, das Listing 12 ist mit dem Listing 8 überschrieben worden.

Das Listing 12 müsste in etwa so aussehen:

public interface MaskedInputFieldResources extends ClientBundle {

public static final MaskedInputFieldResources INSTANCE = GWT.create(MaskedInputFieldResources.class);

@Source(„jquery.min.js“)
TextResource jquery();

@Source(„jquery.inputmask.js“)
TextResource inputmask();
}

Und dann unter src/main/resources/… abzulegen.

Grüße,
Victor Röder