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#
Page contents
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.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>
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>