Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for array parameters to callJsFunction() and executeJs() #20485

Open
archiecobbs opened this issue Nov 15, 2024 · 1 comment
Open

Comments

@archiecobbs
Copy link
Contributor

archiecobbs commented Nov 15, 2024

Describe your motivation

I have a custom JS function that takes an JS array parameter (an array of strings). I tried to invoke it from the server side but it failed with this exception:

java.lang.IllegalArgumentException: Can't encode class [Ljava.lang.String; to json
	at com.vaadin.flow.internal.JsonCodec.encodeWithoutTypeInfo(JsonCodec.java:208) ~[flow-server-23.4.1.jar:23.4.1]
	at com.vaadin.flow.internal.JsonCodec.encodeWithTypeInfo(JsonCodec.java:95) ~[flow-server-23.4.1.jar:23.4.1]
        ...

This was surprising, as there is a direct and obvious way to convert Java arrays into JSON arrays, assuming the elements themselves are convertible.

Describe the solution you'd like

I would like Element.callJsFunction() and Element.executeJs() to support parameters having any array type for which the element type is supported.

Describe alternatives you've considered

Gross hacks like this:

// This is a workaround for https://github.com/vaadin/flow/issues/20485
// Warning: Each occurrence of array parameter $n is replaced with a *distinct* array literal
private PendingJavaScriptResult executeJs(Element element, String expression, Serializable... params) {

    // Initialize
    final List<Serializable> oldParams = Arrays.asList(params);
    final ArrayList<Serializable> newParams = new ArrayList<>();
    final int oldTotal = oldParams.size();
    int newTotal = oldTotal;
    boolean recurse = false;

    // Flatten array elements from oldParams -> newParams, updating expression placeholders as we go
    for (int oldIndex = 0, newIndex = 0; oldIndex < oldTotal; oldIndex++, newIndex++) {
        final Serializable param = oldParams.get(oldIndex);

        // If the param is not an array, just add it and proceed
        if (param == null || !param.getClass().isArray()) {
            newParams.add(param);
            continue;
        }

        // Upshift all parameter placeholders after $newIndex
        final int arrayLength = Array.getLength(param);
        final int indexShift = arrayLength - 1;
        final int minShiftable = newIndex + 1;
        expression = Pattern.compile("(?<=\\$)[0-9]+(?![0-9])").matcher(expression)
          .replaceAll(result -> {
            final int paramIndex = Integer.parseInt(result.group(), 10);
            return paramIndex >= minShiftable ? String.valueOf(paramIndex + indexShift) : result.group();
          });

        // Replace placeholder $newIndex with JS array literal containing new $placeholders
        final String arrayLiteralReplacement = IntStream.range(newIndex, newIndex + arrayLength)
          .mapToObj(index -> "\\$" + index).collect(Collectors.joining(", ", "[ ", " ]"));
        expression = expression.replaceAll("\\$" + newIndex + "(?![0-9])", arrayLiteralReplacement);

        // Add array elements as parameters; if any is an array, then we will need to recurse
        for (int i = 0; i < arrayLength; i++) {
            final Serializable elem = (Serializable)Array.get(param, i);
            recurse |= elem != null && elem.getClass().isArray();
            newParams.add(elem);
        }

        // Update new parameter current index and total
        newIndex += indexShift;
        newTotal += indexShift;
    }
    params = newParams.toArray(new Serializable[newParams.size()]);

    // Recurse if needed, then execute
    return recurse ? this.executeJs(element, expression, params) : element.executeJs(expression, params);
}

Additional context

None.

@Legioth
Copy link
Member

Legioth commented Nov 18, 2024

For a simpler workaround, you can convert your array to a elemental.json.JsonValue and pass that as a parameter since it will then be sent to the browser as-is.

If you're lazy and fine with using a class from com.vaadin.flow.internal, then you can easily convert the array to JSON like this:

JsonValue json = JsonUtils.writeValue(new String[] { "Foo", "Bar" });

getElement().executeJs("console.log($0)", json);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

3 participants