Make it easier to create connectors for integrating custom components
See original GitHub issueWhen integrating a component that has non-trivial functionality, it is usually necessary to use some custom JavaScript logic to connect things together. Implementing connector logic using Element::executeJavaScript
becomes impractical if there are many JS snippets or if any of them are more complex than only a couple of statements. To deal with that, the connector logic can be extracted to a separate JavaScript file that exports functions that can be called using executeJavaScript
.
Developing this kind of connector script could be further simplified by introducing some helper API around the concept.
One good way of structuring the JavaScript logic is as an ES6 class to which the target element instance is passed as a constructor parameter. To avoid naming conflicts, it would be recommended to name the class by converting the targeted custom element name to camel case and appending Connector
, e.g. VaadinTextFieldConnector
.
class VaadinTextFieldConnector {
constructor(element) {
this.element = element;
}
setRequiredIndicatorVisible(visible) {
// ...
}
}
To access the logic defined by the ES6 class, a corresponding Java interface is defined. By default, the simple name of the interface should match the ES6 class name, but a custom ES6 class name can also be configured using an annotation on the interface. The interface should also define resource annotations (typically @JavaScript
) for the files(s) containing the client-side implementation. It might be necessary to supplement the logic that collects resources for the production bundle to also take connector classes into account.
@JavaScript("vaadinTextFieldConnector.js");
public interface VaadinTextFieldConnector extends Connector {
void setRequiredIndicatorVisible(boolean visible);
}
The framework can optionally parse the contents of the JavaScript file and report warnings if the ES6 class doesn’t contain methods corresponding the the methods defined in the Java interface.
To use a connector, the component implementation uses the public <T extends Connector> T createConnector(Class<T> type)
method defined in Element
. This creates a new proxy instance and enqueues commands to create a corresponding client-side instance connected to the target element. The same commands will automatically be sent again if the element has been detached and becomes attached again.
Calling any method on the proxy instance will enqueue a command to call the corresponding JavaScript method. All the same parameter types as for executeJavaScript
are supported. If executeJavaScript
is enhanced to support getting a return value back from the client, then the connector proxy logic should also be updated to use the same mechanism. Until then, all methods in the interface must have void
as their return type.
private final VaadinTextFieldConnector connector = getElement().createConnector(VaadinTextFieldConnector.class);
public void setRequiredIndicatorVisible(boolean visible) {
super.setRequiredIndicatorVisible(visible);
conenctor.setRequiredIndicatorVisible(visible);
}
In some cases, the client-side logic also needs to send messages to the server. To support this case, there is a two-arguments overload of createConnector
that also receives an instance that serves as a reverse RPC target, similar to ServerRpc
in Vaadin 7 and 8. A JavaScript representation of this object will be passed as a second parameter to the ES6 constructor. Calling methods on that object will trigger a request to the server that will invoke the corresponding method on the provided Java instance.
Example interface
public interface GridServerRpc extends ServerRpc {
void select(String key);
void deselect(String key);
}
Usage in the component
public Grid() {
this.connector = getElement().createConnector(VaadinGridConnector.class, new GridServerRpc() {
@Override
public void select(String key) {
getSelectionModel().selectFromClient(findByKey(key));
}
@Override
public void deselect(String key) {
getSelectionModel().deselectFromClient(findByKey(key));
}
});
}
Usage in JavaScript
class VaadinGridConnector {
constructor(element, rpc) {
// Assuming a super-convenient API in the element just to keep this example simple
element.addSelectListener(event => rpc.select(event.key));
element.addDeselectListener(event => rpc.deselect(event.key));
}
}
Issue Analytics
- State:
- Created 5 years ago
- Reactions:2
- Comments:12 (12 by maintainers)
Top GitHub Comments
I like the idea of a generic dispatch method that accepts a method name and an arbitrary list of parameters. I would name it
callFunction
instead ofcallMethod
to align with the very similar method inElement
. This approach would significantly simplify the implementation since there would be no need to mess with dynamically created proxy instances.Support for defining a typesafe interface could be added at a later point if it’s deemed necessary. Another thing of value that it does add is the possibility to automatically check that the method names in the interface are also present in the JavaScript file and that the number or parameters matches. Taken one step further, the warning message from the checker could contain a method stub for anything that is missing from the interface. In that way, the developer could just copy the basic structure into their Java code and fill in the actual type information manually. Full automatic generation would be problematic since the parameter types cannot be inferred from the JavaScript.
getElement().getConnector()
is problematic since it assumes that there’s only one connector instance per element. This is not the case when the super type defines one connector and a subclass defines another. Furthermore, it leaves no (good) way of defining the name of the client-side class.this
is used in thecreateConnector
example since there would be multiple components associated with one element when usingComposite
.This idea does also need some tweaking in light of the ideas needed for ensuring CSP compatibility and improving type safety that are described in https://github.com/vaadin/flow/issues/10759. On the other hand, the proposed syntax of
someElement.getJsInvoker(GreeterJs.class).showGreeting("Hello");
does also satisfy the requirements we have here, namely a way of defining client-side code in a standalone file and then invoking that code in a way that is associated with a target element. The main difference is that there would be no separate “connector” instance but the exported functions would instead be invoked withthis
referencing the target DOM element. A connector implementation that needs its own state could thus hook on to the element instance (preferably by using aWeakMap
, but also possible to just add its own state as a property on the element).The original example with the required indicator could thus be implemented with code like this:
vaadinTextFieldConnector.js
TextFieldConnector.java
And finally usage in the
TextField
component class itselfFor passing values back to the server, it would seem that
@ClientCallable
is a workable mechanism for basically any case even though it’s in theory on the wrong abstraction level with being tied toComponent
rather thanElement
. A pureElement
case can also use eitheraddEventListener
with custom DOM events orReturnChannelMap
from theNode
level for something that might be more flexible.@ClientCallable
also has the added benefit that it offers a natural way of sending a return value back to the client.Taken together, this means that there isn’t really anything specific that would need to be added in the scope of this issue, aside from ensuring there is documentation specifically for this use case.