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.
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.
JQueryResourceHandler#
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>

