JQueryWidget2

JQueryWidget2 ist das neue Werkzeug, um noch effizienter das am weitesten verbreitete Browser-Framework JQuery mit dem am weitesten verbreiteten Business-Framework und Microservices zu kombinieren.

Erstellen Sie eigene Komponenten mit einem hohen Grad an Interaktivität, indem Sie auf JQueryWidget2 aufbauen. Sie erhalten damit eine Programmierschnittstelle (API), mit der Sie sich auf das Wesentliche konzentrieren können.

Inhalt#

Übersicht#

Folgende Möglichkeiten stehen mit JQueryWidget2 zur Verfügung:

Events#

Events können standardmäßig wie in JSF2 verwendet werden. Dazu werden zunächst die entsprechenden Methoden der UIComponent implementiert:
	public static final String DEFAULT_EVENT = "timeout";
	public static final Collection<String> EVENT_NAMES = Arrays.asList(DEFAULT_EVENT);
	
	@Override
	public Collection<String> getEventNames() {
		return EVENT_NAMES;
	}

	@Override
	public String getDefaultEventName() {
		return DEFAULT_EVENT;
	}

Diese Events können später von Tags empfangen werden, wie <j:behavior> oder <p:ajax>.

JQueryWidget2 sendet die entsprechenden Javascript Funktionen direkt an die JQuery Komponente, sodass diese Events im Javascript Code aufgerufen werden können:

	if (widget.options.timeout)
		widget.options.timeout();

Vorher wird abgefragt, ob eine Funktion für diesen Code geliefert wurde. In der Regel ist die Nutzung der Events optional.

Widget Elements#

Eine komplexe Komponente besteht oft aus mehreren Teilen. Diese Teile möchte man getrennt updaten können, Daten mit unterschiedlichen Modellen austauschen können und unterschiedliche Events auslösen. Möchte man Elemente verwendet, implementiert man zunächst NamingContainer in der Komponente:
@FacesComponent(namespace = JQueryWidget2.NAMESPACE, createTag = true)
@ResourceDependencies({
	@ResourceDependency(name = "jquery/jquery.js", library = "primefaces"),
	@ResourceDependency(name = "jsf-jquery.js", library = "jquery-js"),
	@ResourceDependency(name = "jquery.throttle.min.js", library = "jquery-js"),
	@ResourceDependency(name = "matrix.js", library = "jquery-js"),
	@ResourceDependency(name = "matrix.css", library = "jquery-css")
})
public class Matrix extends JQueryWidget2 implements NamingContainer {
	[...]
}

Damit ist es möglich, der Komponente einzelne HTML-Tags hinzuzufügen, die dann ein eindeutiges Prefix besitzen. Dazu wird die Methode addElements überschrieben:

	@Override
	public void addElements(List<UIComponent> elements) {
		elements.add(new WidgetElement<String>("table") {});
	}

Hinweis: WidgetElement ist die einfachste Klasse zum Implementieren eines Elements. Da es sich um eine generische Klasse mit einem Typ-Parameter handelt, muss der Typ-Parameter spezifiziert werden. Da dieser in dem Beispiel nicht wirklich benötigt wird, geben wir hier String an.

Dadurch wird folgender HTML-Code geschrieben:

<span id="form:matrix" class="j-matrix" style="height: 500px;">
	<span id="form:matrix:table" class="j-matrix-table"/>
</span>

Das Element erhält eine Id, die sich aus der Client-Id des JQueryWidget2 zusammensetzt zuzüglich des Namens des Elements selbst: "<client-id>:table".

Im Javascript Code wird dann über das Property elements auf die Elemente zugegriffen, also mit this.elements.table. Darin befindet sich unter anderem das HTML-Element (JQuery-gewrappt):

this.elements.table.element()

Damit kann man bequem alle möglichen Operationen auf dem Element des Widgets ausführen.

AJAX Requests#

Um einen JSF2 konformen AJAX-Request auszuführen, wird ein Element vom Typ WidgetBehavior verwendet.
	@Override
	public void addElements(List<UIComponent> elements) {
		elements.add(new WidgetBehavior<String>("table") {});
	}

Hinweis: Hier wird die Klasse WidgetBehavior implementiert. Der Typparameter erfüllt hier die Aufgabe, den Typ des AJAX-Models festzulegen. Aufgrund der Einfachheit verwenden wir hier wieder String.

JSF verwendet zum Abschicken des Requests immer ein Element mit einer Client-Id. Um bestimmte Operationen auszuführen, wird auch ein Element upgedatet. Die Grundeinstellung bei JQueryWidget2 ist, dass das Element sowohl den AJAX-Request abschickt, als auch upgedated wird. So braucht man zunächst keinerlei Konfiguration vorzunehmen, AJAX läuft also "out of the box":

this.elements.table.request()

Auf der Java-Seite können wir den Aufruf nun empfangen mit:

	@Override
	public void addElements(List<UIComponent> elements) {
		elements.add(new WidgetBehavior<String>("table") {
			@Override
			public void invokeApplication() {
				System.out.println("table was invoked");
			}
		});
	}

WidgetBehavior besitzt folgende Methoden, um den JSF-Lifecycle abzubilden:

void decodeData(T data)
void invokeApplication()
void encodeContent(FacesContext context) throws IOException
Object encodeData()

MethodeBedeutung
decodeDataHier werden die vom Javascript gesendeten JSON-Daten fertig deserialisiert als Java-Objekt geliefert
invokeApplicationAufruf in der Invoke Application Phase
encodeContentHier kann HTML-Content des Elements geschrieben werden
encodeDataHier kann ein Java-Objekt in der Form von JSON-Daten an den Javascript-Code zurückgegeben werden

Auf der Javascript Seite sieht die API dann folgendermaßen aus:

this.elements.table.request({
	data: "Hello",
	callback: function(data) {
		console.log("Response: " + data);
	}
});

Es können also zwei unterschiedliche Klassen für den AJAX-Aufruf und die AJAX-Antwort verwendet werden. Sowohl Request-Parameter als auch Response-Parameter sind jeweils optional und können bei Bedarf verwendet werden. Werden sie nicht gebraucht, können die entsprechenden Methoden einfach weggelassen werden.

Der AJAX-Aufruf erfolgt asynchron. Die Daten werden durch das Property "data" mitgegeben. In unserem Beispiel entspricht der Datentyp String. Es kann hier jede JSON-Deserialisierbares Java-Klasse verwendet werden. Die Antwort erfolgt in der Form eines Callbacks, da die Antwort asynchron kommt. Dazu übergeben wie die Callback-Funktion durch das Property "callback". Die Funktion kann optional das Argument "data" haben, welches dann die AJAX-Response bringt.

DatenJavascriptJava
RequestdatadecodeData(T data)
Responsecallback(data)Object encodeData()

Hinweis: Die AJAX-Response Methode in Java hat den Rückgabetyp Object. Dieser kann beim Überschreiben auf eine Klasse festgelegt werden. Allerdings kann auch weiterhin Object verwendet werden, da die JSON-Serialisierung die Klasse aus dem tatsächlich zurückgegebenen Objekt verwendet.

Method Expression Behavior#

Die Klasse MethodExpressionBehavior ist ein WidgetBehavior, das das direkt mit einer EL-Method Expression verknüpft ist. Das Binding erfolgt an das Attribut mit dem Namen dieses MethodExpressionBehavior.

Value Expression Behavior#

Wie MethodExpressionBehavior, allerdings wird hier eine EL-Value Expression gebunden.

Widget Input#

Dieses Widget Element rendert einen HTML Tag <input> mit dem entsprechenden Attribut value="{<JSON>}" das aus dem Property "value" der Klasse gewonnen wird. Die Methode getValue ist abstrakt und wird bei der Implementierung überschrieben. Beim Abschicken des Input-Field durch einen Form- oder AJAX-Submit wird das Objekt mit einen JSON-Update aktualisiert (siehe Jackson Update).

Widget Stylesheet#

Mit dem Element WidgetStylesheet kann ein Stylesheet dynamisch gerendert werden. Das stellt die Methode encodeStyles(StylesheetWriter writer) zur Verfügung, mit der Bequem CSS-Styles generiert werden können. Bei Komponenten mit viel HTML-Code kann so HTML-Code reduziert werden, indem wiederholte Attribute "style" vermieden werden.

Eigene Widget Elements#

WidgetElement, WidgetBehavior oder die anderen Ableitungen davon können weiter abgeleitet werden um eigene Elemente für das Widget zu generieren.
public class ScrollableElement extends WidgetBehavior<String> {
	public ScrollableElement(String id) {
		super(id);
	}
	
	@Override
	public void encodeContent(FacesContext context) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		String prefix = getClientId() + UINamingContainer.getSeparatorChar(context);
		writer.startElement("div", null);
		writer.writeAttribute("id", prefix + "cells", null);
		writer.writeAttribute("class", getParent().getWidgetStyleClass() + "-cells", prefix);
		writer.endElement("div");
	}
}

Erklärung: Hier wurde WidgetBehavior abgeleitet um ein scrollbares <div>-Tag innerhalb des default-mäßigen <span>-Tags zu erzeugen. Eine entsprechende Client-Id und Style Klassen werden gesetzt.

Options#

JQuery UI bietet die möglichkeit von Options, die innnerhalb der JQuery Komponente mit this.options abgerufen werden. Wie genau die Options von JQuery gehandhabt werden, welche Möglichkeiten es gibt, kann auf deren Web-Seite recherchiert werden.

Von unserer Seite her, also vom JQueryWidget2 gibt es eine Reihe von Möglichkeiten zum Übertragen von Options:

Property Options#

Options werden durch Java-Properties in der Java-Komponente übertragen. Da nicht jedes Property der Komponente per Default übertragen werden kann, werden die gewünschten Properties durch Überschreiben der Methode getOptions angegeben:
	@Override
	public String getOptions(Object... properties) {
		return super.getOptions(properties, "value", "scroll");
	}

Hinweis: Um ein kaskadiertes Überschreiben der Methode zu ermöglichen, werden die der Methode selbst übergebenen Properties auch wieder an den Super-Aufruf übergeben.

Behavior Options#

Unter-Tags die dem aktuellen neue Client-Behaviors (von ClientBehavior abgeleiteten Klassen) hinzufügen, erzeugen Javascript-Funktionen. Beispiele sind der <f:ajax>, <p:ajax> oder <j:behavior>. Diese Javascript Funktionen werden als Option an die JQuery UI Komponente übergeben.
<app:customTable id="table" value="#{customTableController.model}">
	<p:ajax listener="#{customTableController.select}"/>
</app:customTable>

Parameter Options#

Options können durch UIParameter-Komponenten, also <f:param>, <j:function>, <j:json> oder selbst davon abgeleiteten Tags hinzugefügt werden.
<j:matrix id="matrix" value="#{matrixController.model}">
	<j:function name="format" arguments="cell, data">
		cell.text(data.v)
			.css({backgroundColor: data.c});
	</j:function>
</j:matrix>

Element Options#

Die durch addElements hinzugefügten Elemente von einer Ableitung der Klasse WidgetElement haben eine Methode addOptions. Diese Methode kann überschrieben werden, um mit dem WidgetElement assoziierte Options zu übertragen. Diese Optionen sind dann über this.elements.<name>.options abrufbar.
@Override
public void addElements(List<UIComponent> elements) {
	elements.add(new WidgetBehavior<Drop>("drop") {
		@Override
		public void addOptions(Map<String, Object> options) {
			if (getUpdate() != null && !"".equals(getUpdate()))
				options.put("update", BuildingBlockDragDrop.this.findComponent(getUpdate()).getClientId());
		}

		@Override
		public void decodeData(Drop data) {
			if (action != null)
				action.invoke(getFacesContext().getELContext(), new Object[] {data});
		}
	});
}

Widget Options#

Zuletzt gibt es noch die Möglichkeit zum manuellen hinzufügen von Options. Dazu wird die Methode addWidgetOptions überschrieben. Damit erhält man Zugriff auf die Map mit den Options und kann entsprechenden Code einfügen:
@Override
public void addWidgetOptions(Map<String, Object> options) {
	options.put("tree", findComponent(getTree()).getClientId());
}

Hinweis: In der Regel wird die Anwendung einfacher konfigurierbar und modularer, wenn man zunächst die anderen Optionen ausschöpft. Diese Art zum Hinzufügen von Parametern kann für sehr spezifische Optionen verwendet werden, von der kein allgemeiner Gebrauch abgeleitet wird.

Beispiele#

Die folgenden Beispiele verdeutlichen den Einsatz des JQueryWidget2.

Timer#

Der Java-Code für die Komponente:
@FacesComponent(namespace = AppComponent.NAMESPACE, createTag = true)
@ResourceDependencies({
	@ResourceDependency(name = "jquery/jquery.js", library = "primefaces"),
	@ResourceDependency(name = "jsf-jquery.js", library = "jquery-js"),
	@ResourceDependency(name = "jquery.countdown.min.js", library = "js"),
	@ResourceDependency(name = "timer.js", library = "js")
})
public class Timer extends JQueryWidget2 {
    public static final String DEFAULT_EVENT = "timeout";
	public static final Collection<String> EVENT_NAMES = Arrays.asList(DEFAULT_EVENT);
	
	@Override
	public Collection<String> getEventNames() {
		return EVENT_NAMES;
	}

	@Override
	public String getDefaultEventName() {
		return DEFAULT_EVENT;
	}

	public Integer getTime() {
		return (Integer)getStateHelper().eval("time");
	}
	public void setTime(Integer time) {
		getStateHelper().put("time", time);
	}
	
	public String getFormat() {
		return (String)getStateHelper().eval("format");
	}
	public void setFormat(String format) {
		getStateHelper().put("format", format);
	}
	
	public String getOntimeout() {
		return (String)getStateHelper().eval("ontimeout");
	}
	public void setOntimeout(String ontimeout) {
		getStateHelper().put("ontimeout", ontimeout);
	}
	
	@Override
	public String getOptions(Object... properties) {
		return super.getOptions("time", "format", "timeout");
	}
}

Der Javascript-Code für die JQuery-Komponente:

//# sourceURL=timer.js
// Documentation: http://hilios.github.io/jQuery.countdown/documentation.html

$.widget("ext.Timer", {
	options: {
		format: "%M:%S"
	},
	
	_create: function() {
		this._super();
		var widget = this;
		this.element.on("update.countdown", function(event) {
			widget.element.text(event.strftime(widget.options.format));
		});
		this.element.on("finish.countdown", function(event) {
			if (widget.options.timeout)
				widget.options.timeout();
			widget.start();
		});
		if (this.options.time)
			this.start();
	},
	
	start: function() {
		this.element.countdown(Date.now() + 1000 * this.options.time);
	}
});