question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Make it easier to create connectors for integrating custom components

See original GitHub issue

When 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:open
  • Created 5 years ago
  • Reactions:2
  • Comments:12 (12 by maintainers)

github_iconTop GitHub Comments

1reaction
Legiothcommented, Jan 22, 2019

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 of callMethod to align with the very similar method in Element. 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 the createConnector example since there would be multiple components associated with one element when using Composite.

0reactions
Legiothcommented, May 14, 2021

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 with this referencing the target DOM element. A connector implementation that needs its own state could thus hook on to the element instance (preferably by using a WeakMap, 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

export function setRequiredIndicatorVisible(visible) {
  // Actual implementation, using `this` to reference the element
}

TextFieldConnector.java

@JsExpressionModule("./vaadinTextFieldConnector")
public interface TextFieldConnector {
  void setRequiredIndicatorVisible(boolean visible);
}

And finally usage in the TextField component class itself

public void setRequiredIndicatorVisible(boolean visible) {
  super.setRequiredIndicatorVisible(visible);
  getElement().getJsInvoker(TextFieldConnector.class).setRequiredIndicatorVisible(visible);
}

For 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 to Component rather than Element. A pure Element case can also use either addEventListener with custom DOM events or ReturnChannelMap from the Node 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Create a custom connector from scratch - Microsoft Learn
On the left pane, select Data > Custom connectors. Screenshot of Select custom connectors. Select New custom connector, and then select Create ......
Read more >
Writing Custom Components | Prismatic Docs
Prismatic allows dev users to write custom API connectors and other components for their integrations. Learn more about creating custom components with ...
Read more >
Connectors for Integration: What Are Those and What Do They ...
Connectors enable businesses to integrate directly with the APIs of the apps they want to use. The connectors sit between the two APIs....
Read more >
How You Implement a Custom Connector - TechDocs
After you have loaded the Sample connector project into Eclipse, you can begin implementing the domain manager integration for a custom ...
Read more >
A foolproof formula for customizing connectors
Do you want to reduce cost by integrating multiple components? ... Simple custom modifications include incorporating a particular O-ring, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found