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.
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.
@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.
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.
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. |
@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.
$.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}"/>
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>