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.

Achtung: JSF-JQuery 2 wurde durch JQueryWidget2 abgelöst. Verwenden Sie jetzt JQueryWidget2, um noch kompatibler und effizienter JQuery mit JSF zu kombinieren.

Inhalt#

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:

OptionTypWertErklärung
urlcustomgetUrl()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.
dataTypepropertyscriptLegt 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:

OptionTypWertErklärung
stylepropertyemptyMit diesem Tag-Attribut oder Property können Style-Informationen an das Top-Level DIV-Element des Widgets übergeben werden.
styleClasspropertygetWidgetStyleClass()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.

FunktionErklä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>