private JavaScriptObject connectorWrapper;
private int tag;
- private boolean inited = false;
+ private String initFunctionName;
public JavaScriptConnectorHelper(ServerConnector connector) {
this.connector = connector;
}
// Init after setting up callbacks & rpc
- if (!inited) {
+ if (initFunctionName == null) {
initJavaScript();
- inited = true;
}
invokeIfPresent(wrapper, "onStateChange");
return object;
}
- private boolean initJavaScript() {
+ protected boolean initJavaScript() {
ApplicationConfiguration conf = connector.getConnection()
.getConfiguration();
ArrayList<String> attemptedNames = new ArrayList<String>();
if (tryInitJs(initFunctionName, getConnectorWrapper())) {
VConsole.log("JavaScript connector initialized using "
+ initFunctionName);
+ this.initFunctionName = initFunctionName;
return true;
} else {
VConsole.log("No JavaScript function " + initFunctionName
}
}-*/;
- private JavaScriptObject getConnectorWrapper() {
+ public JavaScriptObject getConnectorWrapper() {
if (connectorWrapper == null) {
connectorWrapper = createConnectorWrapper(this,
connector.getConnection(), nativeState, rpcMap,
}
}-*/;
+ public String getInitFunctionName() {
+ return initFunctionName;
+ }
}
--- /dev/null
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.connectors;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.NativeEvent;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.JavaScriptConnectorHelper;
+import com.vaadin.client.Util;
+import com.vaadin.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.ui.renderer.AbstractJavaScriptRenderer;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * Connector for server-side renderer implemented using JavaScript.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(AbstractJavaScriptRenderer.class)
+public class JavaScriptRendererConnector extends
+ AbstractRendererConnector<JsonValue> implements
+ HasJavaScriptConnectorHelper {
+ private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper(
+ this);
+
+ private final JavaScriptObject cellReferenceWrapper = createCellReferenceWrapper(BrowserInfo
+ .get().isIE8());
+
+ @Override
+ protected void init() {
+ super.init();
+ helper.init();
+
+ addGetRowKey(helper.getConnectorWrapper());
+ }
+
+ private static native JavaScriptObject createCellReferenceWrapper(
+ boolean isIE8)
+ /*-{
+ var reference = {};
+ if (isIE8) {
+ // IE8 only supports defineProperty for DOM objects
+ reference = $doc.createElement('div');
+ }
+
+ var setProperty = function(name, getter, setter) {
+ var descriptor = {
+ get: getter
+ }
+ if (setter) {
+ descriptor.set = setter;
+ }
+ Object.defineProperty(reference, name, descriptor);
+ };
+
+ setProperty("element", function() {
+ return reference.target.@CellReference::getElement()();
+ }, null);
+
+ setProperty("rowIndex", function() {
+ return reference.target.@CellReference::getRowIndex()();
+ }, null);
+
+ setProperty("columnIndex", function() {
+ return reference.target.@CellReference::getColumnIndex()();
+ }, null);
+
+ setProperty("colSpan", function() {
+ return reference.target.@RendererCellReference::getColSpan()();
+ }, function(colSpan) {
+ reference.target.@RendererCellReference::setColSpan(*)(colSpan);
+ });
+
+ return reference;
+ }-*/;
+
+ @Override
+ public JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+
+ private native void addGetRowKey(JavaScriptObject wrapper)
+ /*-{
+ var self = this;
+ wrapper.getRowKey = $entry(function(rowIndex) {
+ return @JavaScriptRendererConnector::findRowKey(*)(self, rowIndex);
+ });
+ }-*/;
+
+ private static String findRowKey(JavaScriptRendererConnector connector,
+ int rowIndex) {
+ GridConnector gc = (GridConnector) connector.getParent();
+ JsonObject row = gc.getWidget().getDataSource().getRow(rowIndex);
+ return connector.getRowKey(row);
+ }
+
+ private boolean hasFunction(String name) {
+ return hasFunction(helper.getConnectorWrapper(), name);
+ }
+
+ private static native boolean hasFunction(JavaScriptObject wrapper,
+ String name)
+ /*-{
+ return typeof wrapper[name] === 'function';
+ }-*/;
+
+ @Override
+ protected Renderer<JsonValue> createRenderer() {
+ if (!hasFunction("render")) {
+ throw new RuntimeException("JavaScriptRenderer "
+ + helper.getInitFunctionName()
+ + " must have a function named 'render'");
+ }
+
+ final boolean hasInit = hasFunction("init");
+ final boolean hasDestroy = hasFunction("destroy");
+ final boolean hasOnActivate = hasFunction("onActivate");
+ final boolean hasGetConsumedEvents = hasFunction("getConsumedEvents");
+ final boolean hasOnBrowserEvent = hasFunction("onBrowserEvent");
+
+ return new ComplexRenderer<JsonValue>() {
+ @Override
+ public void render(RendererCellReference cell, JsonValue data) {
+ render(helper.getConnectorWrapper(), getJsCell(cell),
+ Util.json2jso(data));
+ }
+
+ private JavaScriptObject getJsCell(CellReference<?> cell) {
+ updateCellReference(cellReferenceWrapper, cell);
+ return cellReferenceWrapper;
+ }
+
+ public native void render(JavaScriptObject wrapper,
+ JavaScriptObject cell, JavaScriptObject data)
+ /*-{
+ wrapper.render(cell, data);
+ }-*/;
+
+ @Override
+ public void init(RendererCellReference cell) {
+ if (hasInit) {
+ init(helper.getConnectorWrapper(), getJsCell(cell));
+ }
+ }
+
+ private native void init(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ wrapper.init(cell);
+ }-*/;
+
+ private native void updateCellReference(
+ JavaScriptObject cellWrapper, CellReference<?> target)
+ /*-{
+ cellWrapper.target = target;
+ }-*/;
+
+ @Override
+ public void destroy(RendererCellReference cell) {
+ if (hasDestroy) {
+ destory(helper.getConnectorWrapper(), getJsCell(cell));
+ } else {
+ super.destroy(cell);
+ }
+ }
+
+ private native void destory(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ wrapper.destory(cell);
+ }-*/;
+
+ @Override
+ public boolean onActivate(CellReference<?> cell) {
+ if (hasOnActivate) {
+ return onActivate(helper.getConnectorWrapper(),
+ getJsCell(cell));
+ } else {
+ return super.onActivate(cell);
+ }
+ }
+
+ private native boolean onActivate(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ return !!wrapper.onActivate(cell);
+ }-*/;
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ if (hasGetConsumedEvents) {
+ JsArrayString events = getConsumedEvents(helper
+ .getConnectorWrapper());
+
+ ArrayList<String> list = new ArrayList<String>(
+ events.length());
+ for (int i = 0; i < events.length(); i++) {
+ list.add(events.get(i));
+ }
+ return list;
+ } else {
+ return super.getConsumedEvents();
+ }
+ }
+
+ private native JsArrayString getConsumedEvents(
+ JavaScriptObject wrapper)
+ /*-{
+ var rawEvents = wrapper.getConsumedEvents();
+ var events = [];
+ for(var i = 0; i < rawEvents.length; i++) {
+ events[i] = ""+rawEvents[i];
+ }
+ return events;
+ }-*/;
+
+ @Override
+ public boolean onBrowserEvent(CellReference<?> cell,
+ NativeEvent event) {
+ if (hasOnBrowserEvent) {
+ return onBrowserEvent(helper.getConnectorWrapper(),
+ getJsCell(cell), event);
+ } else {
+ return super.onBrowserEvent(cell, event);
+ }
+ }
+
+ private native boolean onBrowserEvent(JavaScriptObject wrapper,
+ JavaScriptObject cell, NativeEvent event)
+ /*-{
+ return !!wrapper.onBrowserEvent(cell, event);
+ }-*/;
+ };
+ }
+
+ @Override
+ public JsonValue decode(JsonValue value) {
+ // Let the js logic decode the raw json that the server sent
+ return value;
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+ helper.onUnregister();
+ }
+
+ @Override
+ public JavaScriptConnectorHelper getJavascriptConnectorHelper() {
+ return helper;
+ }
+}
*
* @return the number of columns that the cell should span
*/
- public int getColspan() {
+ public int getColSpan() {
return cell.getColSpan();
}
}
* <li>Java Dates are represented by JavaScript numbers containing the timestamp
* </li>
* <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li>
- * <li>Map<String, ?> in Java is represented by JavaScript object with fields
- * corresponding to the map keys.</li>
+ * <li>Map<String, ?> in Java is represented by JavaScript object with
+ * fields corresponding to the map keys.</li>
* <li>Any other Java Map is represented by a JavaScript array containing two
* arrays, the first contains the keys and the second contains the values in the
* same order.</li>
* <li>Java Dates are represented by JavaScript numbers containing the timestamp
* </li>
* <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li>
- * <li>Map<String, ?> in Java is represented by JavaScript object with fields
- * corresponding to the map keys.</li>
+ * <li>Map<String, ?> in Java is represented by JavaScript object with
+ * fields corresponding to the map keys.</li>
* <li>Any other Java Map is represented by a JavaScript array containing two
* arrays, the first contains the keys and the second contains the values in the
* same order.</li>
--- /dev/null
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderer;
+
+import com.vaadin.server.AbstractJavaScriptExtension;
+import com.vaadin.server.JavaScriptCallbackHelper;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.ui.Grid.AbstractRenderer;
+import com.vaadin.ui.JavaScriptFunction;
+
+/**
+ * Base class for Renderers with all client-side logic implemented using
+ * JavaScript.
+ * <p>
+ * When a new JavaScript renderer is initialized in the browser, the framework
+ * will look for a globally defined JavaScript function that will initialize the
+ * renderer. The name of the initialization function is formed by replacing .
+ * with _ in the name of the server-side class. If no such function is defined,
+ * each super class is used in turn until a match is found. The framework will
+ * thus first attempt with <code>com_example_MyRenderer</code> for the
+ * server-side
+ * <code>com.example.MyRenderer extends AbstractJavaScriptRenderer</code> class.
+ * If MyRenderer instead extends <code>com.example.SuperRenderer</code> , then
+ * <code>com_example_SuperRenderer</code> will also be attempted if
+ * <code>com_example_MyRenderer</code> has not been defined.
+ * <p>
+ *
+ * In addition to the general JavaScript extension functionality explained in
+ * {@link AbstractJavaScriptExtension}, this class also provides some
+ * functionality specific for renderers.
+ * <p>
+ * The initialization function will be called with <code>this</code> pointing to
+ * a connector wrapper object providing integration to Vaadin with the following
+ * functions:
+ * <ul>
+ * <li><code>getRowKey(rowIndex)</code> - Gets a unique identifier for the row
+ * at the given index. This identifier can be used on the server to retrieve the
+ * corresponding ItemId using {@link #getItemId(String)}.</li>
+ * </ul>
+ * The connector wrapper also supports these special functions that can be
+ * implemented by the connector:
+ * <ul>
+ * <li><code>render(cell, data)</code> - Callback for rendering the given data
+ * into the given cell. The structure of cell and data are described in separate
+ * sections below. The renderer is required to implement this function.
+ * Corresponds to
+ * {@link com.vaadin.client.renderers.Renderer#render(com.vaadin.client.widget.grid.RendererCellReference, Object)}
+ * .</li>
+ * <li><code>init(cell)</code> - Prepares a cell for rendering. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#init(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>destory(cell)</code> - Allows the renderer to release resources
+ * allocate for a cell that will no longer be used. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#destroy(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>onActivate(cell)</code> - Called when the cell is activated by the
+ * user e.g. by double clicking on the cell or pressing enter with the cell
+ * focused. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onActivate(com.vaadin.client.widget.grid.CellReference)}
+ * .</li>
+ * <li><code>getConsumedEvents()</code> - Returns a JavaScript array of event
+ * names that should cause onBrowserEvent to be invoked whenever an event is
+ * fired for a cell managed by this renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#getConsumedEvents()}.</li>
+ * <li><code>onBrowserEvent(cell, event)</code> - Called by Grid when an event
+ * of a type returned by getConsumedEvents is fired for a cell managed by this
+ * renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onBrowserEvent(com.vaadin.client.widget.grid.CellReference, com.google.gwt.dom.client.NativeEvent)}
+ * .</li>
+ * </ul>
+ *
+ * <p>
+ * The cell object passed to functions defined by the renderer has these
+ * properties:
+ * <ul>
+ * <li><code>element</code> - The DOM element corresponding to this cell.
+ * Readonly.</li>
+ * <li><code>rowIndex</code> - The current index of the row of this cell.
+ * Readonly.</li>
+ * <li><code>columnIndex</code> - The current index of the column of this cell.
+ * Readonly.</li>
+ * <li><code>colSpan</code> - The number of columns spanned by this cell. Only
+ * supported in the object passed to the <code>render</code> function - other
+ * functions should not use the property. Readable and writable.
+ * </ul>
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public abstract class AbstractJavaScriptRenderer<T> extends AbstractRenderer<T> {
+ private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper(
+ this);
+
+ protected AbstractJavaScriptRenderer(Class<T> presentationType) {
+ super(presentationType);
+ }
+
+ @Override
+ protected <R extends ServerRpc> void registerRpc(R implementation,
+ Class<R> rpcInterfaceType) {
+ super.registerRpc(implementation, rpcInterfaceType);
+ callbackHelper.registerRpc(rpcInterfaceType);
+ }
+
+ /**
+ * Register a {@link JavaScriptFunction} that can be called from the
+ * JavaScript using the provided name. A JavaScript function with the
+ * provided name will be added to the connector wrapper object (initially
+ * available as <code>this</code>). Calling that JavaScript function will
+ * cause the call method in the registered {@link JavaScriptFunction} to be
+ * invoked with the same arguments.
+ *
+ * @param functionName
+ * the name that should be used for client-side callback
+ * @param function
+ * the {@link JavaScriptFunction} object that will be invoked
+ * when the JavaScript function is called
+ */
+ protected void addFunction(String functionName, JavaScriptFunction function) {
+ callbackHelper.registerCallback(functionName, function);
+ }
+
+ /**
+ * Invoke a named function that the connector JavaScript has added to the
+ * JavaScript connector wrapper object. The arguments should only contain
+ * data types that can be represented in JavaScript including primitives,
+ * their boxed types, arrays, String, List, Set, Map, Connector and
+ * JavaBeans.
+ *
+ * @param name
+ * the name of the function
+ * @param arguments
+ * function arguments
+ */
+ protected void callFunction(String name, Object... arguments) {
+ callbackHelper.invokeCallback(name, arguments);
+ }
+
+ @Override
+ protected JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.components.grid;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+
+public class JavaScriptRenderers extends AbstractTestUI {
+
+ public static class MyBean {
+ private int integer;
+ private String string;
+
+ public MyBean(int integer, String string) {
+ super();
+ this.integer = integer;
+ this.string = string;
+ }
+
+ public int getInteger() {
+ return integer;
+ }
+
+ public void setInteger(int integer) {
+ this.integer = integer;
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(String string) {
+ this.string = string;
+ }
+ }
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty("id", Integer.class, Integer.valueOf(0));
+ container.addContainerProperty("bean", MyBean.class, null);
+
+ for (int i = 0; i < 1000; i++) {
+ Integer itemId = Integer.valueOf(i);
+ Item item = container.addItem(itemId);
+ item.getItemProperty("id").setValue(itemId);
+ item.getItemProperty("bean").setValue(
+ new MyBean(i + 1, Integer.toString(i - 1)));
+ }
+
+ Grid grid = new Grid(container);
+
+ grid.getColumn("bean").setRenderer(new MyBeanJSRenderer());
+ grid.getColumn("bean").setWidth(250);
+
+ addComponent(grid);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class JavaScriptRenderersTest extends MultiBrowserTest {
+
+ @Test
+ public void testJavaScriptRenderer() {
+ setDebug(true);
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ GridCellElement cell_1_2 = grid.getCell(1, 2);
+
+ // Verify render functionality
+ Assert.assertEquals("Bean(2, 0)", cell_1_2.getText());
+
+ // Verify init functionality
+ Assert.assertEquals("2", cell_1_2.getAttribute("column"));
+
+ // Verify onbrowserevent
+ cell_1_2.click();
+ Assert.assertTrue(cell_1_2.getText().startsWith(
+ "Clicked 1 with key 1 at"));
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.components.grid;
+
+import com.vaadin.annotations.JavaScript;
+import com.vaadin.tests.components.grid.JavaScriptRenderers.MyBean;
+import com.vaadin.ui.renderer.AbstractJavaScriptRenderer;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@JavaScript("myBeanJsRenderer.js")
+public class MyBeanJSRenderer extends AbstractJavaScriptRenderer<MyBean> {
+
+ public MyBeanJSRenderer() {
+ super(MyBean.class);
+ }
+
+}
--- /dev/null
+window.com_vaadin_tests_components_grid_MyBeanJSRenderer = function() {
+ this.init = function(cell) {
+ cell.element.setAttribute("column", cell.columnIndex);
+ }
+
+ this.render = function(cell, data) {
+ cell.element.innerHTML = 'Bean(' + data.integer + ', ' + data.string + ')'
+ }
+
+ this.getConsumedEvents = function() { return ["click"] };
+
+ this.onBrowserEvent = function(cell, event) {
+ cell.element.innerHTML = "Clicked " + cell.rowIndex + " with key " + this.getRowKey(cell.rowIndex) +" at " + event.clientX;
+ return true;
+ }
+}
\ No newline at end of file