[JSF-JQuery 2] ist der nächste logische Schritt nach [JSF-JQuery]. Die Version 2 automatisiert die API noch weiter, sodass die Implementierung von kundenspezifischen Komponenten sich auf das Wesentliche konzentrieren kann.

!!!Inhalt
[{TableOfContents title='Page contents' numbered='true'}]

!!!Component Class
Während Version 1 noch auf JQueryComponent basiert, bringt Version 2 eine weitere Ableitung JQueryWidget mit. Die bisherige Klasse kann für mehr Flexibilität weiter genutzt werden, einfacher und bequemer geht es mit der neuen Klasse.

!!Einblick in JQueryWidget
Zunächst die Basisklasse JQueryComponent, diese implementiert, wie gewöhnlich, die JSF-Klasse UIComponentBase und zusätzlich das Interfaces JQueryListener:

{{{
public abstract class JQueryComponent extends UIComponentBase implements JQueryListener
}}}

JQueryListener ist ein Interfaces für einen extrem vereinfachten JQuery-JSON-Request Lifecycle:

{{{
public interface JQueryListener extends FacesListener {
	Object processRequest(FacesContext context, Map<String, String> parameters);
	void renderResponse(FacesContext context, Object result) throws IOException;
}
}}}

Die Aufgabe von processRequest ist es, den JQuery-Request entgegenzunehmen und ein Antwortobjekt zu liefern. Dieses Objekt wird JSON-Serialisiert und an das JQuery-Widget zurückgeschickt. JQueryWidget hat hier eine Default-Implementierung:

{{{
	public Object getValue() {
		return getStateHelper().eval("value");
	}
	public void setValue(Object value) {
		getStateHelper().put("value", value);
	}
	
	@Override
	public Object processRequest(FacesContext context, Map<String, String> parameters) {
		return getValue();
	}
}}}

__Erklärung:__ Bei einem Javascript seitigen Request antwortet die Komponente damit, indem sie das Objekt aus dem Value-Binding des Attributs "value" JSON-Serialisiert und als Response zurück schickt.

__Hinweis:__ Für komplexere Antworten oder generierte Objekte kann die Methode überschrieben werden.

!!Widget Encoding
JQueryWidget liefert nun auch eine Default-Implementierung für die JSF-Methoden encodeBegin und encodeEnd:

{{{
	@Override
	public void encodeBegin(FacesContext context) throws IOException {
		super.encodeBegin(context);
		ResponseWriter writer = context.getResponseWriter();
		writer.startElement("div", this);
		writer.writeAttribute("id", getClientId(), "id");
		[...]
	}
	
	@Override
	public void encodeEnd(FacesContext context) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		writer.startElement("script", this);
		writer.writeAttribute("type", "text/javascript", null);
		String options = getOptions();
		writer.writeText("$(function() {", this, null);
		writer.writeText("$.id('" + getClientId() + "')." + getWidgetName() + "(" + options + ");", this, null);
		writer.writeText("});", this, null);
		writer.endElement("script");

		writer.endElement("div");
		super.encodeEnd(context);
	}
}}}

__Erklärung:__ Zunächst wird ein HTML DIV-Element geschrieben, welches die Client-Id der JSF-Komponente enthält. So kann später die Zuordnung zur Komponente wiederhergestellt werden. Dann wird ein Javascript-Block geschrieben, der die JQuery-Komponente auf dem gerenderten DIV instantiiert. Im Konstruktor werden die Parameter aus der Methode getOptions übergeben, sodass diese später im Javascript unter this.options verfügbar sind.

!!Options
In der Regel bedarf es einiger Optionen, mit denen das Widget gesteuert werden kann. Dazu gibt es mehrere Mechanismen, mit der Tag-Attribute, Attribut-Tags oder Behaviors übergeben werden können.

!Default Options
Eine Reihe von Options sind bereits im JQueryWidget selbst implementiert, weil diese häufig benötigt werden:

||Option||Typ||Wert||Erklärung
|url|custom|getUrl()|Der Kanal über den das Widget mit dem Server kommuniziert. Dieser sollte nur geändert werden, wenn man einen eigenen Resource-Handler implementieren möchte. Dafür sollte man einen guten Grund haben.
|dataType|property|script|Legt den Mime-Type fest, mit dem das Widget mit dem Server kommuniziert. Mögliche Werte sind script, json, xml oder text. Die Funktionalität der Widget-Funktionen sind allerdings nur im Default gewährleistet. Andernfalls sind entsprechende Funktionen zu überschreiben.

Weitere Optionen werden nicht an das Widget übergeben, aber direkt an den vom Java-Code generierten XHTML-Code:

||Option||Typ||Wert||Erklärung
|style|property|empty|Mit diesem Tag-Attribut oder Property können Style-Informationen an das Top-Level DIV-Element des Widgets übergeben werden.
|styleClass|property|getWidgetStyleClass()|Dieses Attribut oder Property übergibt Style-Klassen an das Widget. Default ist, die Methode getWidgetStyleClass() aufzurufen, deren Default-Wert wiederum ist j-<lower widget name>, also der Class-Name des Widget in Kleinschrift.

!Property Options
Dazu kann die Methode getOptions überschrieben werden:

{{{
@Override
public String getOptions(Object... properties) {
	return super.getOptions("customOption");
}
}}}

Alle hier angegebenen Properties werden als Java-Properties der Java-Component an das Javascript-Widget übergeben. Die Implementierung auf der Java-Seite erfolgt im typischen JSF-Style:

{{{
public String getCustomOption() {
	return (String)getStateHelper().eval("customOption");
}
public void setCustomOption(String customOption) {
	getStateHelper().put("customOption", customOption);
}
}}}

Beispiel:

{{{
<j:test customOption="some option"/>
}}}

__Hinweis:__ Jede Option kann ebenfalls eine EL-Expression enthalten.

!Behavior Options
Alle dem Tag untergeordneten Behaviors, also beispielsweise <p:ajax> oder <e:behavior> werden dem Javascript-Widget in Form von Javascript-Funktionen übergeben:

{{{
<j:autocomplete id="autocomplete" value="#{jqueryController.value}" complete="#{jqueryController.complete}">
	<p:ajax event="change" listener="#{jqueryController.action}"/>
</j:autocomplete>
}}}

__Erklärung:__ Das Javascript-Widget implementiert den Event "change". Dieser kann dann auf der XHTML-Seite durch einen entsprechenden Client-Behavior Tag verarbeitet werden, wie das von Tags, wie <h:inputText> gewohnt ist.

!Parameter Options
Eine weitere Möglichkeit zur Übergabe von Optionen ist die Param-Tag-Familie mit den Implementierungen <f:param>, <j:param>, <j:function>:

* __<f:param>:__ Der standardmäßige Parameter-Tag übergibt das Name-Value-Paar 1:1 via JSON an das Widget. Der Value darf dabei eine EL Value-Expression darstellen und ein komplexes Objekt enthalten, das natürlich JSON serialisierbar sein muss.  Dabei können Jackson-Annotationen verwendet werden, um die Serialisierung zu steuern. Insbesondere darf auch das Interface JsonSerializable implementiert werden und die von Jackson mitgelieferte Variante RawValue genutzt werden.
* __<j:function>:__ Dieser Parameter übergibt eine Javascript-Funktion. Da <f:param> die Werte immer zu Strings macht, kann man mit <f:param> keine Javascript-Parameter übergeben, man braucht daher einen anderen Tag, wie eben <j:function>. Dieser Tag unterstützt das Attribut arguments, um die Komma seperierte der Javascript Argumente zu definieren. Der Javascript-Code kann sich sowohl im Value-Attribut (wenig Code) oder im Text-Body des Tags (viel Code) befinden. Für größeren Code sollte man ohnehin eine Javascript-Datei schreiben und aus dem Tag nur referenzieren.
* __<j:param>:__ Mit diesem Tag kann der Value frei übergeben werden. Der Tag sollte nur als allerletzte Lösung verwendet werden, weil dadurch die Gefahr von ungültigem JSON-Code entsteht, der zu einem Javascript-Fehler führt und die Ausführung der Web-Seite verhindert.

Beispiel:

{{{
<j:autocomplete id="autocomplete" value="#{jqueryController.value}" complete="#{jqueryController.complete}">
	<p:inputText id="input" autocomplete="off"/>
	<j:function name="cellRenderer", arguments="$row, key, value">
		if (key == "styleClass") {
			$row.addClass(value);
		} else {
			var $cell = $("<td/>");
			$cell.attr("title", value);
			$cell.html(value);
			if (key == "label") {
				$cell.css("background-color", "black");
				$cell.css("color", "yellow");
			}
			$row.append($cell);
		}
	</j:function>
</j:autocomplete>
}}}

!Custom Options
Die einfachste Möglichkeit um dynamisch berechnete Optionen hinzuzufügen ist es, ein Property-Option zu definieren und den Getter zu implementieren. Aber es gibt noch eine weitere Methode, mit der man Options hinzufügen kann:

{{{
public void addWidgetOptions(Map<String, Object> options) {
    options.put("name", "value");
}
}}}

!!!Styling
Wie beim Property styleClass bereits beschrieben, erhält das Top-Level-DIV des Widgets die Style-Class j-<lower widget name>. Wenn die Widget-Klasse also Autocomplete heißt, bekommt sie die Style-Class j-autocomplete. Damit dürfte ein Großteil der Styling-Regeln im CSS behandelbar sein.

Beispiel:

{{{
.j-autocomplete > .j-autocomplete-panel  {
	display: none;
	z-index: 1;
	max-height: 300px;
	overflow-x: hidden;
	overflow-y: auto;
	white-space: nowrap;
}
}}}

!!!Server Requests
Der Request einer JSF-JQuery Komponente wird nicht vom JSF-Lifecycle entgegengenommen, sondern von einem JSF-Resource-Handler namens JQueryResourceHandler. Es handelt sich um einen Resource-Handler, ähnlich wie bei Image-, Script- oder CSS-Requests.

Dieser "simuliert" einen echten JSF-Lifecycle, jedoch ohne den teueren Komponentenbaum zu rekonstruieren. Damit sind Lifecycle Phasen, Scopes (inklusive View-Scope) und damit EL-Expressions verfügbar.

!!!Javascript Widget
Auf der Javascript-Seite wird das Basis-Widget ext.JQueryWidget mitgeliefert, welches die Kommunikation mit der Komponente übernimmt. Über View-Id, View-State und Client-Id der Komponente wird die Verbindung zur entsprechenden Instanz auf der Java-Seite hergestellt.

!!Action und Onsuccess
Um einen JSON-Request zu starten, wird die Funktion "action" aufgerufen. Es können Parameter übergeben werden, ist jedoch nicht erforderlich. Es wird ein JSON-Request gestartet, der Response kommt über die Funktion onsuccess herein.

||Funktion||Erklärung
|action(options)|Starten eines JSON-Requests an den Server.
|onsuccess(request, response, options)|Empfang des Response vom entsprechenden action-Request.
|onfail(request, response, options, status)|Diese Funktion hat eine Default-Implementierung, die den Fehler in einem Primefaces Growl oder Message-Dialog anzeigt.

!!!Beispiele
Vieles lässt sich leichter anhand von Beispielen erklären.

!!Anychart Komponente
Eine typische Implementierung sieht so aus:

{{{
@FacesComponent(namespace = JQueryComponent.NAMESPACE, createTag = true)
@ResourceDependencies({
	@ResourceDependency(name = "jquery/jquery.js", library = "primefaces"),
	@ResourceDependency(name = "jsf-jquery.js", library = "jquery-js"),
	@ResourceDependency(name = "anychart.min.js", library = "anychart"),
	@ResourceDependency(name = "anychart-themes.min.js", library = "anychart"),
	@ResourceDependency(name = "any-chart.js", library = "anychart")
})
public class AnyChart extends JQueryWidget {
	public String getTheme() {
		return (String)getStateHelper().eval("theme");
	}
	public void setTheme(String theme) {
		getStateHelper().put("theme", theme);
	}
	
	@Override
	public String getOptions(Object... properties) {
		return super.getOptions("theme");
	}
}
}}}

__Erklärung:__ getTheme und setTheme sind typische Tag-Attribute von JSF. In der Methode getOptions können Attribute, Events und Parameter angegeben werden, die automatisch an die JQuery-Komponente übergeben werden.

!!Javascript Widget
Javascript-Seitig wird mit der Widget-Function von JQuery von ext.JQueryWidget abgeleitet:

{{{
$.widget("ext.AnyChart", $.ext.JQueryWidget, {
    options: {
    	theme: "defaultTheme"
	},

	_create: function() {
		this._super();
		this.refresh();
	},
	_destroy: function() {
		this.cleanup();
		this._super();
	},
	_setOptions: function(options) {
		this._super(options);
		this.refresh();
	},
	refresh: function() {
		this.action();
	},
	cleanup: function() {
		if (this.chart) {
			this.chart.dispose();
			this.chart = null;
		}
	},
	
	onsuccess: function(request, response, options) {
		this.cleanup();
		response.chart.container = this.element.clientId();
		anychart.theme(anychart.themes[response.theme || this.options.theme]);
		this.chart = anychart.fromJson(response);
		this.chart.draw();
	}
}
}}}

__Erklärung:__ Nach dem Initialisieren des Widget wird in der Funktion refresh die Funktion action aus der Superklasse ext.JQueryWidget aufgerufen. Diese erzeugt den JSON-Request an die Java-Komponente. Die Antwort kommt über den Call-Back onsuccess zurück. Interessant ist vor allem der Parameter "repsonse", darin befinden sich die Daten, die vom Request an die Komponente zurückgeliefert wurden.

Damit ist die Integration der reinen Javascript-Bibliothek Anychart in eine JSF-Komponente abgeschlossen. Die Komponente kann nun verwendet werden:

{{{
<j:anyChart value="#{anyChartController.chart}"/>
}}}

!!Vis-Network
Vis-Network ist eine Javascript-Bibliothek zum Rendern von Netzwerkdiagrammen mittels HTML5. Im Folgenden ist das entsprechende JQueryWidget aufgezeigt, mit dem die Bibliothenk in JSF integriert wird.

Die Komponente:

{{{
@FacesComponent(namespace = JQueryComponent.NAMESPACE, createTag = true)
@ResourceDependencies({
	@ResourceDependency(name = "jquery/jquery.js", library = "primefaces"),
	@ResourceDependency(name = "jsf-jquery.js", library = "jquery-js"),
	@ResourceDependency(name = "vis.min.js", library = "jquery-js"),
	@ResourceDependency(name = "vis-network.js", library = "jquery-js"),
	@ResourceDependency(name = "vis.min.css", library = "jquery-css")
})
public class VisNetwork extends JQueryWidget implements ClientBehaviorHolder {
	public static class Position {
		private int x;
		private int y;

		public int getX() {
			return x;
		}
		public void setX(int x) {
			this.x = x;
		}
		
		public int getY() {
			return y;
		}
		public void setY(int y) {
			this.y = y;
		}
	}
	
	public static class NodeEvent extends AjaxBehaviorEvent {
		private static final long serialVersionUID = 1L;

		private Node node;
		private Position position;

		public NodeEvent(UIComponent component, Behavior behavior) {
			super(component, behavior);
			setPhaseId(PhaseId.INVOKE_APPLICATION);
		}
		
		public Node getNode() {
			return node;
		}
		public void setNode(Node node) {
			this.node = node;
		}
		
		public Position getPosition() {
			return position;
		}
		public void setPosition(Position position) {
			this.position = position;
		}

		@Override
		public boolean isAppropriateListener(FacesListener facesListener) {
			return (facesListener instanceof AjaxBehaviorListener);
		}

		@Override
		public void processListener(FacesListener listener) {
			if (listener instanceof AjaxBehaviorListener) {
				((AjaxBehaviorListener)listener).processAjaxBehavior(this);
			}
		}
	}
	
	public static final String DEFAULT_EVENT = "click";
	public static final Collection<String> EVENT_NAMES = Arrays.asList(DEFAULT_EVENT);
	
	public Boolean getHover() {
		return (Boolean)getStateHelper().eval("hover");
	}
	public void setHover(Boolean hover) {
		getStateHelper().put("hover", hover);
	}
	
	public String getShape() {
		return (String)getStateHelper().eval("shape");
	}
	public void setShape(String shape) {
		getStateHelper().put("shape", shape);
	}
	
	public String getBackground() {
		return (String)getStateHelper().eval("background");
	}
	public void setBackground(String background) {
		getStateHelper().put("background", background);
	}
	
	public String getBorder() {
		return (String)getStateHelper().eval("border");
	}
	public void setBorder(String border) {
		getStateHelper().put("border", border);
	}
	
	public String getColor() {
		return (String)getStateHelper().eval("color");
	}
	public void setColor(String color) {
		getStateHelper().put("color", color);
	}
	
	public String getOnclick() {
		return (String)getStateHelper().eval("onclick");
	}
	public void setOnclick(String onclick) {
		getStateHelper().put("onclick", onclick);
	}
	
	@Override
	public Collection<String> getEventNames() {
		return EVENT_NAMES;
	}

	@Override
	public String getDefaultEventName() {
		return DEFAULT_EVENT;
	}
	
	@Override
	public String getOptions(Object... properties) {
		return super.getOptions("value", "shape", "background", "border", "color", "click");
	}
	public Node getNode(String id) {
		if (id == null)
			return null;
		for (Node node : ((Network)getValue()).getNodes()) {
			if (id.equals(node.getId()))
				return node;
		}
		return null;
	}
	
	@Override
	public void queueEvent(FacesEvent event) {
		Map<String, String> params = getFacesContext().getExternalContext().getRequestParameterMap();
		NodeEvent nodeEvent = new NodeEvent(this, ((BehaviorEvent)event).getBehavior());
		nodeEvent.setNode(getNode(params.get(getClientId() + ":node")));
		nodeEvent.setPosition(JQueryHelper.fromJson(params.get(getClientId() + ":position"), Position.class));
		nodeEvent.setPhaseId(event.getPhaseId());
		super.queueEvent(nodeEvent);
	}
}
}}}

Das JQuery-Widget:

{{{
$.widget("ext.VisNetwork", $.ext.JQueryWidget, {
	options: {
		hover: true,
		shape: "box",
		background: "#d7ebf9",
		border: "#aed0ea",
		color: "#2779aa"
	},
	
	_create: function() {
		this._super();
		this.render();
	},
	_destroy: function() {
		this.cleanup();
		this._super();
	},
	
	render: function() {
		this.cleanup(); // TODO update possible?
		this.network = this.createNetwork(this.options.value);
		var widget = this;
		this.network.on("click", function(param) {
			widget.onclick(param);
		});
	},
	cleanup: function() {
		if (this.network) {
			this.network.off("click");
			this.network.destroy();
			this.network = null;
		}
	},
	
	createNetwork: function(data) {
		return new vis.Network(this.element[0], data, {
			interaction: {
				hover: this.options.hover
			},
			nodes: {
				shape: this.options.shape,
				color: {
					background: this.options.background,
					border: this.options.border
				},
				font: {
					color: this.options.color
				}
			}
		});
	},
	onclick: function(param) {
		if (param.nodes.length != 0) {
			var data = {};
			data[this.element.attr("id") + ":node"] = param.nodes;
			data[this.element.attr("id") + ":position"] = $.stringify(param.pointer.DOM);
			this.ajaxParam(data);
			this.options.click(param);
		}
	},
	onsuccess: function(request, response, options) {
		this.network.setData(response);
		this.network.redraw();
	}
});
}}}

__Erklärung:__ Die Daten für die eigentliche Grafik werden synchorn mit den Options im HTML-Code durch JSF herausgerendert und im HTTP-Dokument-Request übertragen. Natürlich könnte man auch hier über die Action-Funktion gehen und asynchron nachladen, wie dies im Beispiel für Anychart beschrieben wurde. Die Action-Funktion wird hier jedoch für das Klicken auf die Netzwerkknoten genutzt. Daraus wird eine Faces-Behavior erzeugt, mit der man weitere Elemente auf der JSF-Seite steuern kann.

JSF-Seitig sieht es dann so aus:

{{{
<j:visNetwork id="net" value="#{visNetworkController.network}" style="height: 600px;">
	<p:ajax listener="#{visNetworkController.action}" update="@this"/>
</j:visNetwork>
}}}